Dotfiles update 2025-12-25 15:45

This commit is contained in:
Aaron D. Lee
2025-12-25 15:45:29 -05:00
parent c4ccb4150d
commit b1dc1877d1
17 changed files with 4437 additions and 243 deletions

352
bin/dotfiles-analytics.sh Executable file
View File

@@ -0,0 +1,352 @@
#!/usr/bin/env bash
# ============================================================================
# Enhanced Shell Analytics
# ============================================================================
# Advanced command history analysis with time-based patterns, project grouping,
# and actionable insights.
#
# Usage:
# dotfiles-analytics.sh # Dashboard
# dotfiles-analytics.sh hourly # Commands by hour
# dotfiles-analytics.sh weekly # Usage by day of week
# dotfiles-analytics.sh projects # Group by directory
# dotfiles-analytics.sh trends # Usage trends over time
# ============================================================================
set -e
# Source bootstrap
source "${DOTFILES_HOME:-$HOME/.dotfiles}/zsh/lib/bootstrap.zsh" 2>/dev/null || {
DF_GREEN=$'\033[0;32m' DF_YELLOW=$'\033[1;33m' DF_RED=$'\033[0;31m'
DF_CYAN=$'\033[0;36m' DF_BLUE=$'\033[0;34m' DF_MAGENTA=$'\033[0;35m'
DF_NC=$'\033[0m' DF_DIM=$'\033[2m'
df_print_header() { echo "=== $1 ==="; }
df_print_section() { echo -e "${DF_CYAN}$1:${DF_NC}"; }
df_print_indent() { echo " $1"; }
}
# ============================================================================
# Configuration
# ============================================================================
HISTORY_FILE="${HISTFILE:-$HOME/.zsh_history}"
BASH_HISTORY_FILE="$HOME/.bash_history"
# ============================================================================
# History Parsing
# ============================================================================
# Get zsh history with timestamps
get_zsh_history_with_time() {
if [[ -f "$HISTORY_FILE" ]]; then
# Zsh extended history format: : timestamp:0;command
grep "^:" "$HISTORY_FILE" 2>/dev/null | while read -r line; do
local timestamp=$(echo "$line" | cut -d':' -f2)
local cmd=$(echo "$line" | cut -d';' -f2-)
echo "${timestamp}|${cmd}"
done
fi
}
# Get plain history (command only)
get_history() {
if [[ -f "$HISTORY_FILE" ]]; then
grep "^:" "$HISTORY_FILE" 2>/dev/null | cut -d';' -f2- || cat "$HISTORY_FILE"
elif [[ -f "$BASH_HISTORY_FILE" ]]; then
cat "$BASH_HISTORY_FILE"
fi
}
# ============================================================================
# Analytics Functions
# ============================================================================
# Commands by hour of day
show_hourly() {
df_print_section "Command Usage by Hour of Day"
echo ""
declare -A hour_counts
get_zsh_history_with_time | while IFS='|' read -r timestamp cmd; do
if [[ -n "$timestamp" && "$timestamp" =~ ^[0-9]+$ ]]; then
local hour=$(date -d "@$timestamp" '+%H' 2>/dev/null || date -r "$timestamp" '+%H' 2>/dev/null)
if [[ -n "$hour" ]]; then
echo "$hour"
fi
fi
done | sort | uniq -c | sort -k2 -n | while read -r count hour; do
local bar=""
local bar_len=$((count / 50 + 1))
for ((i=0; i<bar_len && i<40; i++)); do bar+="█"; done
printf " %s:00 %5d ${DF_CYAN}%s${DF_NC}\n" "$hour" "$count" "$bar"
done
echo ""
echo -e "${DF_DIM}Peak hours indicate your most active coding times${DF_NC}"
}
# Commands by day of week
show_weekly() {
df_print_section "Command Usage by Day of Week"
echo ""
local days=("Mon" "Tue" "Wed" "Thu" "Fri" "Sat" "Sun")
declare -A day_counts
# Initialize
for day in "${days[@]}"; do
day_counts[$day]=0
done
get_zsh_history_with_time | while IFS='|' read -r timestamp cmd; do
if [[ -n "$timestamp" && "$timestamp" =~ ^[0-9]+$ ]]; then
local dow=$(date -d "@$timestamp" '+%a' 2>/dev/null || date -r "$timestamp" '+%a' 2>/dev/null)
if [[ -n "$dow" ]]; then
echo "$dow"
fi
fi
done | sort | uniq -c | while read -r count day; do
local bar=""
local bar_len=$((count / 100 + 1))
for ((i=0; i<bar_len && i<40; i++)); do bar+="█"; done
printf " %-3s %6d ${DF_CYAN}%s${DF_NC}\n" "$day" "$count" "$bar"
done
echo ""
}
# Commands grouped by working directory
show_projects() {
df_print_section "Command Usage by Project/Directory"
echo ""
# This requires shell integration that saves PWD with commands
# We'll analyze cd commands as a proxy
df_print_indent "Most visited directories:"
echo ""
get_history | grep "^cd " | awk '{print $2}' | \
sed 's|^~|'"$HOME"'|' | \
sort | uniq -c | sort -rn | head -15 | while read -r count dir; do
# Shorten home paths
local short_dir="${dir/#$HOME/~}"
printf " %5d ${DF_CYAN}%s${DF_NC}\n" "$count" "$short_dir"
done
echo ""
df_print_indent "Git repositories worked on:"
echo ""
get_history | grep -E "^(git|g) " | grep -v "^git config" | \
awk '{print $1, $2}' | sort | uniq -c | sort -rn | head -10 | while read -r count cmd subcmd; do
printf " %5d ${DF_GREEN}%s %s${DF_NC}\n" "$count" "$cmd" "$subcmd"
done
}
# Usage trends over time
show_trends() {
df_print_section "Usage Trends (Last 30 Days)"
echo ""
local today=$(date +%s)
local thirty_days_ago=$((today - 30*24*60*60))
df_print_indent "Commands per day:"
echo ""
get_zsh_history_with_time | while IFS='|' read -r timestamp cmd; do
if [[ -n "$timestamp" && "$timestamp" =~ ^[0-9]+$ ]]; then
if (( timestamp >= thirty_days_ago )); then
date -d "@$timestamp" '+%Y-%m-%d' 2>/dev/null || date -r "$timestamp" '+%Y-%m-%d' 2>/dev/null
fi
fi
done | sort | uniq -c | tail -30 | while read -r count date; do
local bar=""
local bar_len=$((count / 20 + 1))
for ((i=0; i<bar_len && i<30; i++)); do bar+="▪"; done
printf " %s %4d ${DF_CYAN}%s${DF_NC}\n" "$date" "$count" "$bar"
done
}
# Command complexity analysis
show_complexity() {
df_print_section "Command Complexity Analysis"
echo ""
df_print_indent "Simple commands (single word):"
local simple=$(get_history | awk 'NF==1' | wc -l)
echo " $simple"
df_print_indent "Piped commands:"
local piped=$(get_history | grep -c '|' || echo 0)
echo " $piped"
df_print_indent "Commands with redirects:"
local redirects=$(get_history | grep -cE '[<>]' || echo 0)
echo " $redirects"
df_print_indent "Commands with subshells:"
local subshell=$(get_history | grep -cE '\$\(' || echo 0)
echo " $subshell"
echo ""
df_print_section "Most Complex Commands (by pipe count)"
echo ""
get_history | awk -F'|' 'NF>2 {print NF-1, $0}' | sort -rn | head -5 | while read -r pipes cmd; do
local short_cmd="${cmd:0:60}"
[[ ${#cmd} -gt 60 ]] && short_cmd="${short_cmd}..."
echo -e " ${DF_MAGENTA}$pipes pipes:${DF_NC} $short_cmd"
done
}
# Tool usage breakdown
show_tools() {
df_print_section "Tool Usage Breakdown"
echo ""
local categories=(
"Git:git g"
"Docker:docker docker-compose d dc"
"Package:pacman paru yay npm pip cargo"
"Editor:vim nvim vi nano code"
"Navigation:cd ls ll la cat less"
"System:sudo systemctl journalctl"
"Network:curl wget ssh scp"
)
for category in "${categories[@]}"; do
local name="${category%%:*}"
local tools="${category#*:}"
local total=0
for tool in $tools; do
local count=$(get_history | awk '{print $1}' | grep -c "^${tool}$" 2>/dev/null || echo 0)
total=$((total + count))
done
if (( total > 0 )); then
printf " %-12s ${DF_GREEN}%6d${DF_NC}\n" "$name:" "$total"
fi
done
}
# Suggestions based on usage
show_suggestions() {
df_print_section "Optimization Suggestions"
echo ""
# Find frequently typed long commands
df_print_indent "Consider creating aliases for:"
echo ""
get_history | awk 'length > 20' | sort | uniq -c | sort -rn | head -5 | while read -r count cmd; do
if (( count >= 5 )); then
local short_cmd="${cmd:0:50}"
[[ ${#cmd} -gt 50 ]] && short_cmd="${short_cmd}..."
echo -e " ${DF_YELLOW}$count×${DF_NC} $short_cmd"
fi
done
echo ""
# Check for common mistakes
df_print_indent "Common typos detected:"
echo ""
local typos=("gti:git" "sl:ls" "cta:cat" "grpe:grep" "suod:sudo")
for typo_pair in "${typos[@]}"; do
local typo="${typo_pair%%:*}"
local correct="${typo_pair#*:}"
local count=$(get_history | grep -c "^${typo} " 2>/dev/null || echo 0)
if (( count > 0 )); then
echo -e " ${DF_RED}$typo${DF_NC}$correct (${count}×)"
fi
done
}
# Full dashboard
show_dashboard() {
local total=$(get_history | wc -l)
local unique=$(get_history | sort -u | wc -l)
df_print_section "Shell Analytics Dashboard"
echo ""
echo -e " Total commands: ${DF_GREEN}$total${DF_NC}"
echo -e " Unique commands: ${DF_GREEN}$unique${DF_NC}"
echo -e " Efficiency ratio: ${DF_CYAN}$(( unique * 100 / (total + 1) ))%${DF_NC}"
echo ""
df_print_section "Top 15 Commands"
echo ""
get_history | awk '{print $1}' | sort | uniq -c | sort -rn | head -15 | while read -r count cmd; do
printf " %-20s ${DF_GREEN}%5d${DF_NC}\n" "$cmd" "$count"
done
echo ""
show_tools
}
# ============================================================================
# Help
# ============================================================================
show_help() {
cat << 'EOF'
Enhanced Shell Analytics
Usage: dotfiles-analytics.sh [COMMAND]
Commands:
dashboard Full analytics dashboard (default)
hourly Command usage by hour of day
weekly Command usage by day of week
projects Commands grouped by directory
trends Usage trends over last 30 days
complexity Command complexity analysis
tools Tool usage breakdown
suggestions Optimization suggestions
help Show this help
Examples:
dotfiles-analytics.sh # Full dashboard
dotfiles-analytics.sh hourly # See peak coding hours
dotfiles-analytics.sh suggestions # Get alias suggestions
EOF
}
# ============================================================================
# Main
# ============================================================================
main() {
df_print_header "dotfiles-analytics"
if [[ ! -f "$HISTORY_FILE" && ! -f "$BASH_HISTORY_FILE" ]]; then
echo "No history file found"
exit 1
fi
case "${1:-dashboard}" in
dashboard|d) show_dashboard ;;
hourly|h) show_hourly ;;
weekly|w) show_weekly ;;
projects|p) show_projects ;;
trends|t) show_trends ;;
complexity|c) show_complexity ;;
tools) show_tools ;;
suggestions|s) show_suggestions ;;
help|--help|-h) show_help ;;
*)
echo "Unknown command: $1"
show_help
exit 1
;;
esac
}
main "$@"

428
bin/dotfiles-diff.sh Executable file
View File

@@ -0,0 +1,428 @@
#!/usr/bin/env bash
# ============================================================================
# Dotfiles Diff & Audit Tool
# ============================================================================
# Compare configurations, audit for issues, and track changes.
#
# Usage:
# dotfiles-diff.sh # Show uncommitted changes
# dotfiles-diff.sh --symlinks # Verify symlink integrity
# dotfiles-diff.sh --secrets # Audit for exposed secrets
# dotfiles-diff.sh --permissions # Check file permissions
# dotfiles-diff.sh --audit # Full security audit
# ============================================================================
set -e
# Source bootstrap
source "${DOTFILES_HOME:-$HOME/.dotfiles}/zsh/lib/bootstrap.zsh" 2>/dev/null || {
DF_GREEN=$'\033[0;32m' DF_YELLOW=$'\033[1;33m' DF_RED=$'\033[0;31m'
DF_CYAN=$'\033[0;36m' DF_BLUE=$'\033[0;34m' DF_NC=$'\033[0m'
DF_DIM=$'\033[2m'
DOTFILES_HOME="${DOTFILES_HOME:-$HOME/.dotfiles}"
df_print_header() { echo "=== $1 ==="; }
df_print_success() { echo -e "${DF_GREEN}${DF_NC} $1"; }
df_print_error() { echo -e "${DF_RED}${DF_NC} $1" >&2; }
df_print_warning() { echo -e "${DF_YELLOW}${DF_NC} $1"; }
df_print_step() { echo -e "${DF_BLUE}==>${DF_NC} $1"; }
df_print_section() { echo -e "${DF_CYAN}$1:${DF_NC}"; }
df_print_indent() { echo " $1"; }
}
DOTFILES_DIR="${DOTFILES_HOME:-$HOME/.dotfiles}"
# ============================================================================
# Diff Functions
# ============================================================================
# Show git diff for uncommitted changes
show_git_diff() {
df_print_section "Uncommitted Changes"
if [[ ! -d "$DOTFILES_DIR/.git" ]]; then
df_print_warning "Not a git repository"
return 1
fi
cd "$DOTFILES_DIR"
local changes=$(git status --porcelain 2>/dev/null)
if [[ -z "$changes" ]]; then
df_print_success "No uncommitted changes"
return 0
fi
echo ""
echo "$changes" | while read -r status file; do
case "$status" in
M*|" M") echo -e " ${DF_YELLOW}modified:${DF_NC} $file" ;;
A*|"A ") echo -e " ${DF_GREEN}added:${DF_NC} $file" ;;
D*|" D") echo -e " ${DF_RED}deleted:${DF_NC} $file" ;;
R*) echo -e " ${DF_BLUE}renamed:${DF_NC} $file" ;;
\?\?) echo -e " ${DF_DIM}untracked:${DF_NC} $file" ;;
*) echo -e " ${status}: $file" ;;
esac
done
echo ""
df_print_step "View full diff: git -C $DOTFILES_DIR diff"
}
# Show what's different between repo and installed files
show_installed_diff() {
df_print_section "Installed File Differences"
echo ""
local files_to_check=(
"$HOME/.zshrc:$DOTFILES_DIR/zsh/.zshrc"
"$HOME/.gitconfig:$DOTFILES_DIR/git/.gitconfig"
"$HOME/.vimrc:$DOTFILES_DIR/vim/.vimrc"
"$HOME/.tmux.conf:$DOTFILES_DIR/tmux/.tmux.conf"
)
local has_diff=false
for pair in "${files_to_check[@]}"; do
local installed="${pair%%:*}"
local source="${pair#*:}"
local name=$(basename "$installed")
if [[ ! -e "$installed" ]]; then
echo -e " ${DF_YELLOW}${DF_NC} $name: not installed"
continue
fi
if [[ -L "$installed" ]]; then
local target=$(readlink -f "$installed")
if [[ "$target" == "$source" ]] || [[ "$target" == "$(readlink -f "$source")" ]]; then
echo -e " ${DF_GREEN}${DF_NC} $name: symlink OK"
else
echo -e " ${DF_YELLOW}${DF_NC} $name: symlink points elsewhere → $target"
has_diff=true
fi
else
# Regular file - check if different
if diff -q "$installed" "$source" &>/dev/null; then
echo -e " ${DF_GREEN}${DF_NC} $name: content matches (regular file)"
else
echo -e " ${DF_RED}${DF_NC} $name: differs from source"
has_diff=true
fi
fi
done
if [[ "$has_diff" == true ]]; then
echo ""
df_print_warning "Some files differ. Run installer to sync: ./install.sh"
fi
}
# ============================================================================
# Symlink Verification
# ============================================================================
check_symlinks() {
df_print_section "Symlink Integrity Check"
echo ""
local symlinks=(
"$HOME/.zshrc"
"$HOME/.gitconfig"
"$HOME/.vimrc"
"$HOME/.tmux.conf"
"$HOME/.config/nvim"
)
local broken=0
local missing=0
local ok=0
for link in "${symlinks[@]}"; do
local name=$(basename "$link")
if [[ ! -e "$link" && ! -L "$link" ]]; then
echo -e " ${DF_DIM}${DF_NC} $name: not installed"
((missing++))
elif [[ -L "$link" ]]; then
if [[ -e "$link" ]]; then
echo -e " ${DF_GREEN}${DF_NC} $name$(readlink "$link")"
((ok++))
else
echo -e " ${DF_RED}${DF_NC} $name: BROKEN → $(readlink "$link")"
((broken++))
fi
else
echo -e " ${DF_YELLOW}${DF_NC} $name: regular file (not symlink)"
fi
done
# Check bin scripts
echo ""
df_print_section "Bin Script Symlinks"
if [[ -d "$HOME/.local/bin" ]]; then
for script in "$HOME/.local/bin"/dotfiles-*.sh; do
[[ -e "$script" ]] || continue
local name=$(basename "$script")
if [[ -L "$script" ]]; then
if [[ -e "$script" ]]; then
echo -e " ${DF_GREEN}${DF_NC} $name"
((ok++))
else
echo -e " ${DF_RED}${DF_NC} $name: BROKEN"
((broken++))
fi
fi
done
fi
echo ""
df_print_section "Summary"
df_print_indent "OK: $ok | Missing: $missing | Broken: $broken"
if (( broken > 0 )); then
echo ""
df_print_error "Found $broken broken symlinks!"
df_print_indent "Fix with: dffix"
fi
}
# ============================================================================
# Security Audit
# ============================================================================
audit_secrets() {
df_print_section "Secret Detection Audit"
echo ""
cd "$DOTFILES_DIR"
local issues=0
# Patterns that might indicate secrets
local patterns=(
'api[_-]?key\s*[:=]'
'secret[_-]?key\s*[:=]'
'password\s*[:=]'
'token\s*[:=]'
'private[_-]?key'
'BEGIN (RSA|DSA|EC|OPENSSH) PRIVATE KEY'
'aws_access_key_id'
'aws_secret_access_key'
)
df_print_step "Scanning tracked files..."
for pattern in "${patterns[@]}"; do
local matches=$(git grep -l -i -E "$pattern" 2>/dev/null || true)
if [[ -n "$matches" ]]; then
echo ""
df_print_warning "Pattern '$pattern' found in:"
echo "$matches" | while read -r file; do
df_print_indent " $file"
((issues++))
done
fi
done
if (( issues == 0 )); then
df_print_success "No obvious secrets found in tracked files"
else
echo ""
df_print_error "Found potential secrets in $issues location(s)"
df_print_indent "Review these files and use the vault for sensitive data"
fi
# Check git history (limited)
echo ""
df_print_step "Scanning recent git history (last 50 commits)..."
local history_issues=$(git log -50 --all -p 2>/dev/null | grep -c -i -E 'password|secret|api.?key|token' || echo 0)
if (( history_issues > 0 )); then
df_print_warning "Found $history_issues potential matches in git history"
df_print_indent "Consider: git filter-branch or BFG Repo Cleaner"
else
df_print_success "No obvious secrets in recent history"
fi
}
audit_permissions() {
df_print_section "File Permission Audit"
echo ""
cd "$DOTFILES_DIR"
local issues=0
# Check for world-writable files
df_print_step "Checking for world-writable files..."
local world_writable=$(find . -type f -perm -o+w 2>/dev/null | grep -v ".git" || true)
if [[ -n "$world_writable" ]]; then
df_print_warning "World-writable files found:"
echo "$world_writable" | while read -r file; do
df_print_indent "$file"
((issues++))
done
else
df_print_success "No world-writable files"
fi
# Check bin scripts are executable
echo ""
df_print_step "Checking bin script permissions..."
for script in "$DOTFILES_DIR/bin"/*.sh; do
[[ -f "$script" ]] || continue
local name=$(basename "$script")
if [[ -x "$script" ]]; then
echo -e " ${DF_GREEN}${DF_NC} $name"
else
echo -e " ${DF_RED}${DF_NC} $name: not executable"
((issues++))
fi
done
# Check sensitive directories
echo ""
df_print_step "Checking sensitive directories..."
if [[ -d "$DOTFILES_DIR/vault" ]]; then
local vault_perms=$(stat -c %a "$DOTFILES_DIR/vault" 2>/dev/null || stat -f %Lp "$DOTFILES_DIR/vault" 2>/dev/null)
if [[ "$vault_perms" == "700" ]]; then
df_print_success "vault/ directory: 700 (secure)"
else
df_print_warning "vault/ directory: $vault_perms (should be 700)"
((issues++))
fi
fi
echo ""
if (( issues == 0 )); then
df_print_success "All permission checks passed"
else
df_print_warning "Found $issues permission issues"
fi
}
full_audit() {
df_print_step "Running full security audit..."
echo ""
audit_secrets
echo ""
audit_permissions
echo ""
check_symlinks
}
# ============================================================================
# Machine Comparison
# ============================================================================
compare_machines() {
df_print_section "Machine Configuration Comparison"
echo ""
local machines_dir="$DOTFILES_DIR/machines"
if [[ ! -d "$machines_dir" ]] || [[ -z "$(ls -A "$machines_dir" 2>/dev/null)" ]]; then
df_print_info "No machine configs to compare"
df_print_indent "Create with: df_machine_create"
return
fi
local configs=("$machines_dir"/*.zsh(N))
if (( ${#configs[@]} < 2 )); then
df_print_info "Need at least 2 machine configs to compare"
return
fi
df_print_step "Available configs:"
for config in "${configs[@]}"; do
df_print_indent "$(basename "$config" .zsh)"
done
echo ""
read -p "Compare which two? (e.g., 'laptop server'): " config1 config2
if [[ -f "$machines_dir/$config1.zsh" && -f "$machines_dir/$config2.zsh" ]]; then
echo ""
diff -u --color=always "$machines_dir/$config1.zsh" "$machines_dir/$config2.zsh" || true
else
df_print_error "Config not found"
fi
}
# ============================================================================
# Help
# ============================================================================
show_help() {
cat << 'EOF'
Dotfiles Diff & Audit Tool
Usage: dotfiles-diff.sh [OPTIONS]
Options:
(none) Show uncommitted git changes
--installed Compare installed files with source
--symlinks Verify symlink integrity
--secrets Scan for exposed secrets
--permissions Check file permissions
--audit Full security audit (secrets + permissions + symlinks)
--machines Compare machine configurations
--help Show this help
Examples:
dotfiles-diff.sh # Quick git status
dotfiles-diff.sh --symlinks # Verify all symlinks are valid
dotfiles-diff.sh --audit # Full security audit
dotfiles-diff.sh --machines # Compare laptop vs server configs
EOF
}
# ============================================================================
# Main
# ============================================================================
main() {
df_print_header "dotfiles-diff"
case "${1:-}" in
--installed|-i)
show_installed_diff
;;
--symlinks|-s)
check_symlinks
;;
--secrets)
audit_secrets
;;
--permissions|-p)
audit_permissions
;;
--audit|-a)
full_audit
;;
--machines|-m)
compare_machines
;;
--help|-h)
show_help
;;
*)
show_git_diff
echo ""
show_installed_diff
;;
esac
}
main "$@"

300
bin/dotfiles-profile.sh Executable file
View File

@@ -0,0 +1,300 @@
#!/usr/bin/env bash
# ============================================================================
# Dotfiles Startup Profiler
# ============================================================================
# Measures and analyzes shell startup time to identify slow components.
#
# Usage:
# dotfiles-profile.sh # Quick profile
# dotfiles-profile.sh --detailed # Detailed zprof output
# dotfiles-profile.sh --benchmark # Multiple runs with hyperfine
# dotfiles-profile.sh --compare # Compare with minimal shell
# ============================================================================
set -e
# Source bootstrap
source "${DOTFILES_HOME:-$HOME/.dotfiles}/zsh/lib/bootstrap.zsh" 2>/dev/null || {
DF_GREEN=$'\033[0;32m' DF_YELLOW=$'\033[1;33m' DF_RED=$'\033[0;31m'
DF_CYAN=$'\033[0;36m' DF_BLUE=$'\033[0;34m' DF_NC=$'\033[0m'
DF_DIM=$'\033[2m'
df_print_header() { echo "=== $1 ==="; }
df_print_success() { echo -e "${DF_GREEN}${DF_NC} $1"; }
df_print_warning() { echo -e "${DF_YELLOW}${DF_NC} $1"; }
df_print_step() { echo -e "${DF_BLUE}==>${DF_NC} $1"; }
df_print_section() { echo -e "${DF_CYAN}$1:${DF_NC}"; }
df_print_indent() { echo " $1"; }
}
# ============================================================================
# Configuration
# ============================================================================
PROFILE_RUNS=5
SLOW_THRESHOLD_MS=200
VERY_SLOW_THRESHOLD_MS=500
# ============================================================================
# Profiling Functions
# ============================================================================
# Quick timing measurement
quick_profile() {
df_print_section "Quick Startup Timing"
echo ""
local times=()
for i in $(seq 1 $PROFILE_RUNS); do
local start=$(date +%s%3N)
zsh -i -c 'exit' 2>/dev/null
local end=$(date +%s%3N)
local duration=$((end - start))
times+=($duration)
printf " Run %d: ${DF_CYAN}%dms${DF_NC}\n" "$i" "$duration"
done
# Calculate average
local sum=0
for t in "${times[@]}"; do
sum=$((sum + t))
done
local avg=$((sum / PROFILE_RUNS))
echo ""
df_print_section "Results"
if (( avg < SLOW_THRESHOLD_MS )); then
df_print_indent "Average: ${DF_GREEN}${avg}ms${DF_NC} (excellent)"
elif (( avg < VERY_SLOW_THRESHOLD_MS )); then
df_print_indent "Average: ${DF_YELLOW}${avg}ms${DF_NC} (acceptable)"
else
df_print_indent "Average: ${DF_RED}${avg}ms${DF_NC} (slow - optimization recommended)"
fi
}
# Detailed zprof analysis
detailed_profile() {
df_print_section "Detailed Function Profiling (zprof)"
echo ""
# Create temporary profile script
local tmp_zshrc=$(mktemp)
cat > "$tmp_zshrc" << 'EOF'
zmodload zsh/zprof
source ~/.zshrc
zprof
EOF
ZDOTDIR=$(dirname "$tmp_zshrc") zsh -i -c "source $tmp_zshrc; exit" 2>/dev/null | head -40
rm -f "$tmp_zshrc"
echo ""
df_print_info "Top 40 functions shown. Run with ZDOTDIR override for full output."
}
# Benchmark with hyperfine
benchmark_profile() {
if ! command -v hyperfine &>/dev/null; then
df_print_warning "hyperfine not installed"
df_print_info "Install: sudo pacman -S hyperfine"
df_print_info "Falling back to quick profile..."
echo ""
quick_profile
return
fi
df_print_section "Benchmark (hyperfine)"
echo ""
hyperfine --warmup 3 --min-runs 10 \
--export-markdown /tmp/zsh-bench.md \
'zsh -i -c exit' \
2>&1
echo ""
df_print_success "Results saved to /tmp/zsh-bench.md"
}
# Compare with minimal shell
compare_profile() {
df_print_section "Comparison: Full vs Minimal Shell"
echo ""
df_print_step "Full shell (with dotfiles):"
local full_start=$(date +%s%3N)
zsh -i -c 'exit' 2>/dev/null
local full_end=$(date +%s%3N)
local full_time=$((full_end - full_start))
df_print_indent "${full_time}ms"
df_print_step "Minimal shell (no rc files):"
local min_start=$(date +%s%3N)
zsh --no-rcs -i -c 'exit' 2>/dev/null
local min_end=$(date +%s%3N)
local min_time=$((min_end - min_start))
df_print_indent "${min_time}ms"
local overhead=$((full_time - min_time))
local overhead_pct=$((overhead * 100 / (min_time + 1)))
echo ""
df_print_section "Analysis"
df_print_indent "Shell baseline: ${min_time}ms"
df_print_indent "Dotfiles overhead: ${overhead}ms (+${overhead_pct}%)"
if (( overhead > VERY_SLOW_THRESHOLD_MS )); then
echo ""
df_print_warning "High overhead detected. Consider:"
df_print_indent "• Lazy-loading heavy plugins (nvm, kubectl, etc.)"
df_print_indent "• Compiling zsh files: dfcompile"
df_print_indent "• Reducing oh-my-zsh plugins"
df_print_indent "• Using zsh-defer for non-critical loads"
fi
}
# Identify slow components
analyze_components() {
df_print_section "Component Analysis"
echo ""
local components=(
"oh-my-zsh:source \$ZSH/oh-my-zsh.sh"
"autosuggestions:source */zsh-autosuggestions.zsh"
"syntax-highlight:source */zsh-syntax-highlighting.zsh"
"fzf:source */fzf/*.zsh"
"nvm:source \$NVM_DIR/nvm.sh"
"dotfiles-funcs:source */functions/*.zsh"
)
for comp in "${components[@]}"; do
local name="${comp%%:*}"
local pattern="${comp#*:}"
# Time loading this component
local start=$(date +%s%3N)
zsh -i -c "
# Disable the component by commenting it out temporarily
# This is a simplified check
" 2>/dev/null
local end=$(date +%s%3N)
printf " %-20s: checking...\n" "$name"
done
echo ""
df_print_info "For detailed component timing, use: --detailed"
}
# Show optimization tips
show_tips() {
df_print_section "Optimization Tips"
echo ""
cat << 'EOF'
1. COMPILE ZSH FILES
Run: dfcompile
Compiles .zsh files to .zwc bytecode for faster parsing.
2. LAZY-LOAD HEAVY TOOLS
nvm, pyenv, rbenv, kubectl - only load when first used.
Example in .zshrc:
kubectl() { unfunction kubectl; source <(command kubectl completion zsh); kubectl "$@"; }
3. REDUCE OH-MY-ZSH PLUGINS
Each plugin adds startup time. Only enable what you use.
Heavy plugins: nvm, kubectl, docker-compose, thefuck
4. USE ZSH-DEFER
Defer non-critical loading until after first prompt:
zsh-defer source ~/.dotfiles/zsh/functions/heavy-stuff.zsh
5. PROFILE REGULARLY
Run this script after changes to track impact.
6. CHECK FOR SLOW COMPLETIONS
Completion initialization can be slow:
autoload -Uz compinit
if [[ -n ~/.zcompdump(#qN.mh+24) ]]; then
compinit
else
compinit -C # Skip security check (faster)
fi
EOF
}
# ============================================================================
# Help
# ============================================================================
show_help() {
cat << 'EOF'
Dotfiles Startup Profiler
Usage: dotfiles-profile.sh [OPTIONS]
Options:
(none) Quick profile (5 runs, average time)
--detailed Detailed zprof function-level profiling
--benchmark Benchmark with hyperfine (if installed)
--compare Compare full shell vs minimal shell
--tips Show optimization tips
--all Run all profiling methods
--help Show this help
Thresholds:
< 200ms Excellent (green)
200-500ms Acceptable (yellow)
> 500ms Slow (red) - optimization recommended
Examples:
dotfiles-profile.sh # Quick timing
dotfiles-profile.sh --detailed # See which functions are slow
dotfiles-profile.sh --compare # See dotfiles overhead
dotfiles-profile.sh --all # Full analysis
EOF
}
# ============================================================================
# Main
# ============================================================================
main() {
df_print_header "dotfiles-profile"
case "${1:-}" in
--detailed|-d)
detailed_profile
;;
--benchmark|-b)
benchmark_profile
;;
--compare|-c)
compare_profile
;;
--tips|-t)
show_tips
;;
--all|-a)
quick_profile
echo ""
compare_profile
echo ""
detailed_profile
echo ""
show_tips
;;
--help|-h)
show_help
;;
*)
quick_profile
echo ""
df_print_info "For more analysis: $0 --all"
;;
esac
}
main "$@"

480
bin/dotfiles-tour.sh Executable file
View File

@@ -0,0 +1,480 @@
#!/usr/bin/env bash
# ============================================================================
# Dotfiles First-Run Experience & Tour
# ============================================================================
# Provides a guided introduction for new users and after updates.
#
# Usage:
# dotfiles-tour.sh # Interactive tour
# dotfiles-tour.sh --quick # Quick feature overview
# dotfiles-tour.sh --changelog # Show recent changes
# ============================================================================
set -e
# Source bootstrap
source "${DOTFILES_HOME:-$HOME/.dotfiles}/zsh/lib/bootstrap.zsh" 2>/dev/null || {
DF_GREEN=$'\033[0;32m' DF_YELLOW=$'\033[1;33m' DF_RED=$'\033[0;31m'
DF_CYAN=$'\033[0;36m' DF_BLUE=$'\033[0;34m' DF_MAGENTA=$'\033[0;35m'
DF_NC=$'\033[0m' DF_DIM=$'\033[2m' DF_BOLD=$'\033[1m'
DOTFILES_HOME="${DOTFILES_HOME:-$HOME/.dotfiles}"
DOTFILES_VERSION="${DOTFILES_VERSION:-1.0.0}"
df_print_header() { echo "=== $1 ==="; }
df_print_section() { echo -e "${DF_CYAN}$1:${DF_NC}"; }
df_print_indent() { echo " $1"; }
df_print_success() { echo -e "${DF_GREEN}${DF_NC} $1"; }
df_print_info() { echo -e "${DF_CYAN}${DF_NC} $1"; }
}
DOTFILES_DIR="${DOTFILES_HOME:-$HOME/.dotfiles}"
FIRST_RUN_FILE="$DOTFILES_DIR/.initialized"
LAST_VERSION_FILE="$DOTFILES_DIR/.last-version"
# ============================================================================
# Welcome Screen
# ============================================================================
show_welcome() {
clear
cat << 'EOF'
╔═══════════════════════════════════════════════════════════════╗
║ ║
║ █████╗ ██████╗ ██╗ ███████╗███████╗ ║
║ ██╔══██╗██╔══██╗██║ ██╔════╝██╔════╝ ║
║ ███████║██║ ██║██║ █████╗ █████╗ ║
║ ██╔══██║██║ ██║██║ ██╔══╝ ██╔══╝ ║
║ ██║ ██║██████╔╝███████╗███████╗███████╗ ║
║ ╚═╝ ╚═╝╚═════╝ ╚══════╝╚══════╝╚══════╝ ║
║ ║
║ D O T F I L E S ║
║ ║
╚═══════════════════════════════════════════════════════════════╝
EOF
echo ""
echo -e " ${DF_DIM}Version: ${DOTFILES_VERSION}${DF_NC}"
echo ""
echo -e " Welcome to ADLee's Dotfiles!"
echo -e " ${DF_DIM}A productive development environment for Arch/CachyOS${DF_NC}"
echo ""
}
# ============================================================================
# Tour Pages
# ============================================================================
tour_navigation() {
clear
df_print_header "Navigation & Shortcuts"
echo ""
df_print_section "Directory Navigation"
echo ""
echo -e " ${DF_CYAN}..${DF_NC} Go up one directory"
echo -e " ${DF_CYAN}...${DF_NC} Go up two directories"
echo -e " ${DF_CYAN}~${DF_NC} Go to home"
echo -e " ${DF_CYAN}c.${DF_NC} Go to dotfiles directory"
echo ""
df_print_section "Bookmarks"
echo ""
echo -e " ${DF_CYAN}bookmark add work ~/projects/work${DF_NC}"
echo -e " ${DF_CYAN}j work${DF_NC} Jump to bookmark"
echo -e " ${DF_CYAN}bm list${DF_NC} List all bookmarks"
echo ""
df_print_section "Command Palette (Ctrl+Space)"
echo ""
echo -e " Fuzzy search through:"
echo -e " • Aliases and functions"
echo -e " • Command history"
echo -e " • Bookmarks"
echo -e " • Quick actions"
}
tour_dotfiles_management() {
clear
df_print_header "Dotfiles Management"
echo ""
df_print_section "Quick Commands"
echo ""
echo -e " ${DF_GREEN}dfd${DF_NC} Health check (doctor)"
echo -e " ${DF_GREEN}dffix${DF_NC} Auto-fix issues"
echo -e " ${DF_GREEN}dfu${DF_NC} Update dotfiles"
echo -e " ${DF_GREEN}dfs${DF_NC} Sync status"
echo -e " ${DF_GREEN}dfpush${DF_NC} Push changes"
echo -e " ${DF_GREEN}dfpull${DF_NC} Pull changes"
echo ""
df_print_section "Quick Edit"
echo ""
echo -e " ${DF_CYAN}v.zshrc${DF_NC} Edit ~/.zshrc"
echo -e " ${DF_CYAN}v.conf${DF_NC} Edit dotfiles.conf"
echo -e " ${DF_CYAN}v.alias${DF_NC} Edit aliases"
echo -e " ${DF_CYAN}reload${DF_NC} Reload shell config"
echo ""
df_print_section "Machine-Specific Config"
echo ""
echo -e " ${DF_CYAN}machine-info${DF_NC} Show current machine detection"
echo -e " ${DF_CYAN}machine-create${DF_NC} Create config for this machine"
}
tour_git_helpers() {
clear
df_print_header "Git & Development"
echo ""
df_print_section "Git Shortcuts"
echo ""
echo -e " ${DF_GREEN}g${DF_NC} = git"
echo -e " ${DF_GREEN}gs${DF_NC} = git status"
echo -e " ${DF_GREEN}ga${DF_NC} = git add"
echo -e " ${DF_GREEN}gc${DF_NC} = git commit"
echo -e " ${DF_GREEN}gp${DF_NC} = git push"
echo -e " ${DF_GREEN}gl${DF_NC} = git pull"
echo -e " ${DF_GREEN}gd${DF_NC} = git diff"
echo -e " ${DF_GREEN}glog${DF_NC} = pretty log graph"
echo ""
df_print_section "Project Templates"
echo ""
echo -e " ${DF_CYAN}py-new myproject${DF_NC} Basic Python"
echo -e " ${DF_CYAN}py-flask myapp${DF_NC} Flask web app"
echo -e " ${DF_CYAN}py-fastapi myapi${DF_NC} FastAPI REST"
echo -e " ${DF_CYAN}py-cli mytool${DF_NC} CLI with Click"
echo -e " ${DF_CYAN}py-data analysis${DF_NC} Data science"
echo ""
df_print_section "Project Environments"
echo ""
echo -e " Auto-loads ${DF_CYAN}.dotfiles-local${DF_NC} when entering directories"
echo -e " Auto-activates Python virtualenvs"
echo -e " Auto-switches Node versions via .nvmrc"
}
tour_tmux_workspaces() {
clear
df_print_header "Tmux Workspaces"
echo ""
df_print_section "Quick Commands"
echo ""
echo -e " ${DF_GREEN}tw myproject${DF_NC} Create/attach workspace"
echo -e " ${DF_GREEN}tw myproject dev${DF_NC} Create with 'dev' template"
echo -e " ${DF_GREEN}twl${DF_NC} List workspaces"
echo -e " ${DF_GREEN}twf${DF_NC} Fuzzy search workspaces"
echo -e " ${DF_GREEN}tws mytemplate${DF_NC} Save current layout"
echo ""
df_print_section "Built-in Templates"
echo ""
echo -e " ${DF_CYAN}dev${DF_NC} Editor + terminal + logs"
echo -e " ${DF_CYAN}ops${DF_NC} 4-pane monitoring grid"
echo -e " ${DF_CYAN}review${DF_NC} Side-by-side comparison"
echo -e " ${DF_CYAN}debug${DF_NC} Main (70%) + helper (30%)"
echo -e " ${DF_CYAN}ssh-multi${DF_NC} 4 panes for servers"
echo ""
df_print_section "Tmuxinator (if installed)"
echo ""
echo -e " ${DF_CYAN}txi myproject${DF_NC} Start tmuxinator project"
echo -e " ${DF_CYAN}txi-new myproj dev${DF_NC} Create from template"
}
tour_system_tools() {
clear
df_print_header "System Administration"
echo ""
df_print_section "Systemd Helpers"
echo ""
echo -e " ${DF_GREEN}sc${DF_NC} sudo systemctl"
echo -e " ${DF_GREEN}scr${DF_NC} svc Restart + status"
echo -e " ${DF_GREEN}sce${DF_NC} svc Enable + start"
echo -e " ${DF_GREEN}scd${DF_NC} svc Disable + stop"
echo -e " ${DF_GREEN}sclog${DF_NC} svc Follow logs"
echo -e " ${DF_GREEN}scf${DF_NC} Interactive (fzf)"
echo -e " ${DF_GREEN}sc-failed${DF_NC} Show failed services"
echo ""
df_print_section "Btrfs & Snapshots (if using btrfs)"
echo ""
echo -e " ${DF_CYAN}btrfs-health${DF_NC} Quick filesystem check"
echo -e " ${DF_CYAN}snap 'desc'${DF_NC} Create snapshot"
echo -e " ${DF_CYAN}snapls${DF_NC} List snapshots"
echo -e " ${DF_CYAN}sys-update${DF_NC} Update with pre/post snapshot"
echo ""
df_print_section "SSH Manager"
echo ""
echo -e " ${DF_CYAN}ssh-save myserver user@host${DF_NC}"
echo -e " ${DF_CYAN}sshc myserver${DF_NC} Connect (auto-tmux)"
echo -e " ${DF_CYAN}sshf${DF_NC} Fuzzy search servers"
}
tour_productivity() {
clear
df_print_header "Productivity Features"
echo ""
df_print_section "Smart Suggestions"
echo ""
echo -e " Auto-corrects common typos: ${DF_DIM}gti → git${DF_NC}"
echo -e " Suggests packages for missing commands"
echo -e " Use ${DF_CYAN}fuck${DF_NC} to re-run corrected command"
echo ""
df_print_section "Long-Running Command Notifications"
echo ""
echo -e " Desktop notifications when commands take > 60s"
echo -e " ${DF_CYAN}notify-toggle${DF_NC} Enable/disable"
echo -e " ${DF_CYAN}notify-status${DF_NC} Check configuration"
echo ""
df_print_section "Password Manager (LastPass)"
echo ""
echo -e " ${DF_CYAN}pw search${DF_NC} Search and copy password"
echo -e " ${DF_CYAN}pwf${DF_NC} Fuzzy search (fzf)"
echo -e " ${DF_CYAN}pw gen 24${DF_NC} Generate 24-char password"
echo ""
df_print_section "Secrets Vault"
echo ""
echo -e " ${DF_CYAN}vault init${DF_NC} Initialize encrypted vault"
echo -e " ${DF_CYAN}vault set KEY${DF_NC} Store a secret"
echo -e " ${DF_CYAN}vault get KEY${DF_NC} Retrieve a secret"
}
tour_complete() {
clear
df_print_header "Tour Complete!"
echo ""
df_print_success "You're ready to go!"
echo ""
df_print_section "Quick Reference"
echo ""
echo -e " ${DF_CYAN}dfd${DF_NC} Run health check"
echo -e " ${DF_CYAN}Ctrl+Space${DF_NC} Command palette"
echo -e " ${DF_CYAN}dotfiles-cli help${DF_NC} Full command list"
echo ""
df_print_section "Documentation"
echo ""
echo -e " ${DF_CYAN}~/.dotfiles/README.md${DF_NC}"
echo -e " ${DF_CYAN}~/.dotfiles/docs/REFERENCE.md${DF_NC}"
echo ""
df_print_section "Getting Help"
echo ""
echo -e " Most commands support ${DF_CYAN}--help${DF_NC}"
echo -e " Check ${DF_CYAN}~/.dotfiles/INSTALL.md${DF_NC} for troubleshooting"
echo ""
# Mark first run complete
touch "$FIRST_RUN_FILE"
echo "$DOTFILES_VERSION" > "$LAST_VERSION_FILE"
}
# ============================================================================
# Interactive Tour
# ============================================================================
run_interactive_tour() {
local pages=(
"show_welcome:Welcome"
"tour_navigation:Navigation & Shortcuts"
"tour_dotfiles_management:Dotfiles Management"
"tour_git_helpers:Git & Development"
"tour_tmux_workspaces:Tmux Workspaces"
"tour_system_tools:System Administration"
"tour_productivity:Productivity Features"
"tour_complete:Complete"
)
local current=0
local total=${#pages[@]}
while true; do
local page_info="${pages[$current]}"
local func="${page_info%%:*}"
local title="${page_info#*:}"
# Show current page
$func
# Navigation footer
echo ""
echo -e "${DF_DIM}─────────────────────────────────────────────────────────────${DF_NC}"
echo -e " Page $((current + 1)) of $total: ${DF_CYAN}$title${DF_NC}"
echo ""
if (( current == total - 1 )); then
echo -e " Press ${DF_GREEN}Enter${DF_NC} to finish, ${DF_CYAN}p${DF_NC} for previous, ${DF_RED}q${DF_NC} to quit"
else
echo -e " Press ${DF_GREEN}Enter${DF_NC} for next, ${DF_CYAN}p${DF_NC} for previous, ${DF_RED}q${DF_NC} to quit"
fi
read -rsn1 key
case "$key" in
q|Q)
echo ""
echo "Tour cancelled. Run 'dotfiles-tour.sh' anytime to continue."
exit 0
;;
p|P)
(( current > 0 )) && ((current--))
;;
*)
if (( current == total - 1 )); then
echo ""
echo -e "${DF_GREEN}Enjoy your new shell!${DF_NC}"
exit 0
fi
((current++))
;;
esac
done
}
# ============================================================================
# Quick Overview
# ============================================================================
show_quick_overview() {
df_print_header "Quick Feature Overview"
echo ""
cat << 'EOF'
╭──────────────────────────────────────────────────────────────╮
│ NAVIGATION │
│ Ctrl+Space Command palette j <bookmark> Jump │
│ .. Up directory c. Dotfiles dir │
├──────────────────────────────────────────────────────────────┤
│ DOTFILES │
│ dfd Health check dfu Update │
│ dfpush Push changes reload Reload shell │
├──────────────────────────────────────────────────────────────┤
│ GIT │
│ gs Status glog Pretty log │
│ ga/gc/gp Add/commit/push gd Diff │
├──────────────────────────────────────────────────────────────┤
│ TMUX │
│ tw <name> Create workspace twl List │
│ twf Fuzzy search tws Save layout │
├──────────────────────────────────────────────────────────────┤
│ SYSTEM │
│ sc systemctl scf Interactive │
│ scr <svc> Restart service sc-failed Show failed │
├──────────────────────────────────────────────────────────────┤
│ PYTHON │
│ py-new Basic project py-flask Flask app │
│ py-fastapi REST API venv Activate env │
╰──────────────────────────────────────────────────────────────╯
EOF
echo ""
df_print_info "Run 'dotfiles-tour.sh' for full interactive tour"
}
# ============================================================================
# Changelog
# ============================================================================
show_changelog() {
df_print_header "Recent Changes"
echo ""
cd "$DOTFILES_DIR"
local last_version=""
[[ -f "$LAST_VERSION_FILE" ]] && last_version=$(cat "$LAST_VERSION_FILE")
if [[ -n "$last_version" && "$last_version" != "$DOTFILES_VERSION" ]]; then
echo -e "Updated from ${DF_YELLOW}$last_version${DF_NC} to ${DF_GREEN}$DOTFILES_VERSION${DF_NC}"
echo ""
fi
df_print_section "Recent Commits"
echo ""
if [[ -d .git ]]; then
git log --oneline -15 2>/dev/null | while read -r line; do
echo -e " ${DF_DIM}${DF_NC} $line"
done
else
echo " (git history not available)"
fi
echo ""
# Update version tracking
echo "$DOTFILES_VERSION" > "$LAST_VERSION_FILE"
}
# ============================================================================
# First Run Check
# ============================================================================
check_first_run() {
if [[ ! -f "$FIRST_RUN_FILE" ]]; then
echo ""
echo -e "${DF_CYAN}Welcome!${DF_NC} This appears to be your first time using these dotfiles."
echo -e "Run ${DF_GREEN}dotfiles-tour.sh${DF_NC} for a quick introduction."
echo ""
fi
}
# ============================================================================
# Help
# ============================================================================
show_help() {
cat << 'EOF'
Dotfiles Tour & First-Run Experience
Usage: dotfiles-tour.sh [OPTIONS]
Options:
(none) Interactive tour
--quick, -q Quick feature overview
--changelog Show recent changes
--check Check if first run (for .zshrc)
--help Show this help
The tour introduces all major features of the dotfiles system.
Run it anytime to refresh your memory!
EOF
}
# ============================================================================
# Main
# ============================================================================
main() {
case "${1:-}" in
--quick|-q)
df_print_header "dotfiles-tour"
show_quick_overview
;;
--changelog|-c)
df_print_header "dotfiles-tour"
show_changelog
;;
--check)
check_first_run
;;
--help|-h)
show_help
;;
*)
run_interactive_tour
;;
esac
}
main "$@"