From b93026f51f64280bdbe4e27dd4066bb3c2b3b5b6 Mon Sep 17 00:00:00 2001 From: "Aaron D. Lee" Date: Thu, 25 Dec 2025 16:11:04 -0500 Subject: [PATCH] Dotfiles update 2025-12-25 16:11 --- bin/dotfiles-analytics.sh | 300 ++++++++++++++++++++++++-------------- bin/dotfiles-diff.sh | 11 +- 2 files changed, 202 insertions(+), 109 deletions(-) diff --git a/bin/dotfiles-analytics.sh b/bin/dotfiles-analytics.sh index c1d53c9..4d0fc9a 100755 --- a/bin/dotfiles-analytics.sh +++ b/bin/dotfiles-analytics.sh @@ -11,9 +11,13 @@ # dotfiles-analytics.sh weekly # Usage by day of week # dotfiles-analytics.sh projects # Group by directory # dotfiles-analytics.sh trends # Usage trends over time +# dotfiles-analytics.sh complexity # Command complexity analysis +# dotfiles-analytics.sh tools # Tool usage breakdown +# dotfiles-analytics.sh suggestions # Alias suggestions # ============================================================================ -set -e +# Don't exit on error - we handle errors ourselves +set +e # Source bootstrap source "${DOTFILES_HOME:-$HOME/.dotfiles}/zsh/lib/bootstrap.zsh" 2>/dev/null || { @@ -51,9 +55,22 @@ get_zsh_history_with_time() { # Get plain history (command only) get_history() { if [[ -f "$HISTORY_FILE" ]]; then - grep "^:" "$HISTORY_FILE" 2>/dev/null | cut -d';' -f2- || cat "$HISTORY_FILE" + grep "^:" "$HISTORY_FILE" 2>/dev/null | cut -d';' -f2- || cat "$HISTORY_FILE" 2>/dev/null elif [[ -f "$BASH_HISTORY_FILE" ]]; then - cat "$BASH_HISTORY_FILE" + cat "$BASH_HISTORY_FILE" 2>/dev/null + fi +} + +# Safe count function - handles grep errors gracefully +safe_count() { + local pattern="$1" + local result + result=$(get_history | awk '{print $1}' | grep -c "^${pattern}$" 2>/dev/null) || result=0 + # Ensure we return a valid number + if [[ "$result" =~ ^[0-9]+$ ]]; then + echo "$result" + else + echo "0" fi } @@ -66,11 +83,10 @@ 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) + local hour + hour=$(date -d "@$timestamp" '+%H' 2>/dev/null || date -r "$timestamp" '+%H' 2>/dev/null) if [[ -n "$hour" ]]; then echo "$hour" fi @@ -92,16 +108,11 @@ show_weekly() { 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) + local dow + dow=$(date -d "@$timestamp" '+%a' 2>/dev/null || date -r "$timestamp" '+%a' 2>/dev/null) if [[ -n "$dow" ]]; then echo "$dow" fi @@ -110,64 +121,58 @@ show_weekly() { local bar="" local bar_len=$((count / 100 + 1)) for ((i=0; i/dev/null | \ + sed 's|/[^/]*$||' | sort | uniq -c | sort -rn | head -10 | \ + while read -r count dir; do + local short_dir="$dir" + if [[ ${#dir} -gt 40 ]]; then + short_dir="...${dir: -37}" + fi + 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 +# Usage trends over time (last 30 days) 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 "" + local thirty_days_ago=$(($(date +%s) - 30*24*60*60)) 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 + if [[ -n "$timestamp" && "$timestamp" =~ ^[0-9]+$ && "$timestamp" -ge "$thirty_days_ago" ]]; then + local date_str + date_str=$(date -d "@$timestamp" '+%Y-%m-%d' 2>/dev/null || date -r "$timestamp" '+%Y-%m-%d' 2>/dev/null) + if [[ -n "$date_str" ]]; then + echo "$date_str" 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"* || "$cmd" == *"<"* ]]; then + ((redirected++)) || true + elif [[ "$cmd" == *'$('* || "$cmd" == *'`'* ]]; then + ((subshell++)) || true + else + ((simple++)) || true + fi + done < <(get_history) - 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" + if [[ $total -gt 0 ]]; then + echo -e " Simple commands: ${DF_GREEN}$simple${DF_NC} ($((simple * 100 / total))%)" + echo -e " Piped commands: ${DF_YELLOW}$piped${DF_NC} ($((piped * 100 / total))%)" + echo -e " With redirection: ${DF_CYAN}$redirected${DF_NC} ($((redirected * 100 / total))%)" + echo -e " With subshells: ${DF_MAGENTA}$subshell${DF_NC} ($((subshell * 100 / total))%)" + else + echo " No history data available" + fi echo "" - df_print_section "Most Complex Commands (by pipe count)" + + # Most complex commands (by pipe count) + df_print_indent "Most Complex Pipelines:" 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 + get_history | awk -F'|' 'NF > 3 {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 @@ -217,17 +238,25 @@ show_tools() { "Network:curl wget ssh scp" ) + # Cache history to avoid repeated reads + local history_cache + history_cache=$(get_history | awk '{print $1}') + 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)) + local count + count=$(echo "$history_cache" | grep -c "^${tool}$" 2>/dev/null) || count=0 + # Validate count is a number + if [[ "$count" =~ ^[0-9]+$ ]]; then + total=$((total + count)) + fi done - if (( total > 0 )); then + if [[ $total -gt 0 ]]; then printf " %-12s ${DF_GREEN}%6d${DF_NC}\n" "$name:" "$total" fi done @@ -243,10 +272,10 @@ show_suggestions() { echo "" get_history | awk 'length > 20' | sort | uniq -c | sort -rn | head -5 | while read -r count cmd; do - if (( count >= 5 )); then + if [[ "$count" =~ ^[0-9]+$ ]] && [[ $count -ge 5 ]]; then local short_cmd="${cmd:0:50}" [[ ${#cmd} -gt 50 ]] && short_cmd="${short_cmd}..." - echo -e " ${DF_YELLOW}$count×${DF_NC} $short_cmd" + echo -e " ${DF_YELLOW}${count}×${DF_NC} $short_cmd" fi done @@ -257,43 +286,78 @@ show_suggestions() { echo "" local typos=("gti:git" "sl:ls" "cta:cat" "grpe:grep" "suod:sudo") + local found_typos=0 + + # Cache history + local history_cache + history_cache=$(get_history | awk '{print $1}') + 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}×)" + local count + count=$(echo "$history_cache" | grep -c "^${typo}$" 2>/dev/null) || count=0 + + if [[ "$count" =~ ^[0-9]+$ ]] && [[ $count -gt 0 ]]; then + echo -e " ${DF_RED}$typo${DF_NC} → ${DF_GREEN}$correct${DF_NC} (${count}×)" + found_typos=1 fi done + + if [[ $found_typos -eq 0 ]]; then + echo -e " ${DF_GREEN}No common typos detected!${DF_NC}" + fi + + echo "" } -# Full dashboard +# Dashboard view show_dashboard() { - local total=$(get_history | wc -l) - local unique=$(get_history | sort -u | wc -l) - df_print_section "Shell Analytics Dashboard" echo "" + + # Basic stats + local total + total=$(get_history | wc -l 2>/dev/null) || total=0 + local unique + unique=$(get_history | sort -u | wc -l 2>/dev/null) || unique=0 + + # Ensure we have valid numbers + [[ ! "$total" =~ ^[0-9]+$ ]] && total=0 + [[ ! "$unique" =~ ^[0-9]+$ ]] && unique=0 + + # Trim whitespace + total=$(echo "$total" | tr -d ' ') + unique=$(echo "$unique" | tr -d ' ') + 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 -e " Unique commands: ${DF_CYAN}$unique${DF_NC}" + + if [[ $total -gt 0 ]]; then + local efficiency=$((unique * 100 / total)) + echo -e " Efficiency ratio: ${DF_YELLOW}${efficiency}%${DF_NC}" + fi + echo "" + # Top commands 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 + get_history | awk '{print $1}' | sort 2>/dev/null | uniq -c 2>/dev/null | sort -rn 2>/dev/null | head -15 | \ + while read -r count cmd; do + if [[ "$count" =~ ^[0-9]+$ ]] && [[ -n "$cmd" ]]; then + printf " ${DF_GREEN}%-20s${DF_NC} %5d\n" "$cmd" "$count" + fi + done echo "" + + # Tool usage show_tools } -# ============================================================================ # Help -# ============================================================================ - show_help() { cat << 'EOF' Enhanced Shell Analytics @@ -301,20 +365,20 @@ Enhanced Shell Analytics Usage: dotfiles-analytics.sh [COMMAND] Commands: - dashboard Full analytics dashboard (default) + (none) Show dashboard with overview 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 + complexity Analyze command complexity + tools Tool/category usage breakdown + suggestions Alias and optimization suggestions + all Show all analytics Examples: - dotfiles-analytics.sh # Full dashboard - dotfiles-analytics.sh hourly # See peak coding hours - dotfiles-analytics.sh suggestions # Get alias suggestions + dotfiles-analytics.sh # Dashboard + dotfiles-analytics.sh hourly # When do you code most? + dotfiles-analytics.sh suggestions # Get optimization tips EOF } @@ -326,24 +390,48 @@ EOF 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 ;; + dashboard|dash) + show_dashboard + ;; + hourly|hour|h) + show_hourly + ;; + weekly|week|w) + show_weekly + ;; + projects|proj|p) + show_projects + ;; + trends|trend|t) + show_trends + ;; + complexity|complex|c) + show_complexity + ;; + tools|tool) + show_tools + ;; + suggestions|suggest|s) + show_suggestions + ;; + all|a) + show_dashboard + echo "" + show_hourly + echo "" + show_weekly + echo "" + show_complexity + echo "" + show_suggestions + ;; + help|--help|-h) + show_help + ;; *) echo "Unknown command: $1" - show_help + echo "Use --help for usage" exit 1 ;; esac diff --git a/bin/dotfiles-diff.sh b/bin/dotfiles-diff.sh index 06d1346..13e334a 100755 --- a/bin/dotfiles-diff.sh +++ b/bin/dotfiles-diff.sh @@ -12,7 +12,7 @@ # dotfiles-diff.sh --audit # Full security audit # ============================================================================ -set -e +set +e # Source bootstrap source "${DOTFILES_HOME:-$HOME/.dotfiles}/zsh/lib/bootstrap.zsh" 2>/dev/null || { @@ -24,6 +24,7 @@ source "${DOTFILES_HOME:-$HOME/.dotfiles}/zsh/lib/bootstrap.zsh" 2>/dev/null || 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_info() { echo -e "${DF_CYAN}ℹ${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"; } @@ -336,9 +337,13 @@ compare_machines() { return fi - local configs=("$machines_dir"/*.zsh(N)) + # Get list of config files (bash-compatible) + local configs=() + for f in "$machines_dir"/*.zsh; do + [[ -f "$f" ]] && configs+=("$f") + done - if (( ${#configs[@]} < 2 )); then + if [[ ${#configs[@]} -lt 2 ]]; then df_print_info "Need at least 2 machine configs to compare" return fi