diff --git a/README.md b/README.md index 96bf0ba..65300c6 100644 --- a/README.md +++ b/README.md @@ -1,296 +1,406 @@ -# ADLee's Dotfiles +# Dotfiles Improvements -Personal configuration for a productive development environment on **Arch Linux** and **CachyOS**. +This directory contains suggested improvements for your dotfiles project. These additions enhance functionality, maintainability, and user experience. -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -[![Shell](https://img.shields.io/badge/Shell-Zsh-green.svg)](https://www.zsh.org/) -[![OS](https://img.shields.io/badge/OS-Arch%20%2F%20CachyOS-blue.svg)](https://archlinux.org/) +## Summary of Additions -``` -┌[alee@battlestation]─[~/.dotfiles ⎇ main]─[⇑3] -└% -``` +| Category | Files Added | Description | +|----------|-------------|-------------| +| Machine Config | `zsh/lib/machines.zsh`, `machines/*.zsh` | Per-machine configuration support | +| Performance | `bin/dotfiles-profile.sh` | Startup time profiling | +| Notifications | `zsh/functions/notifications.zsh` | Long-running command notifications | +| Security | `bin/dotfiles-diff.sh` | Diff, audit, and secret detection | +| Project Env | `zsh/functions/project-env.zsh` | Auto-load project environments | +| Analytics | `bin/dotfiles-analytics.sh` | Enhanced history analytics | +| Testing | `tests/run-tests.zsh`, `tests/test_*.zsh` | Unit testing framework | +| First-Run | `bin/dotfiles-tour.sh` | Interactive tour and changelog | +| FZF Extras | `zsh/functions/fzf-extras.zsh` | Additional fuzzy finders | +| Plugin Mgr | `zsh/lib/plugins.zsh` | Lightweight plugin management | -## Quick Start +--- + +## Installation + +### Option 1: Copy All Files ```bash -git clone https://github.com/adlee-was-taken/dotfiles.git ~/.dotfiles -cd ~/.dotfiles && ./install.sh +# Backup first +cp -r ~/.dotfiles ~/.dotfiles.backup.$(date +%Y%m%d) + +# Copy improvements +cp -r /path/to/improvements/* ~/.dotfiles/ ``` -See [INSTALL.md](INSTALL.md) for detailed instructions. +### Option 2: Selective Installation + +Copy only the features you want: + +```bash +# Machine-specific configs +cp zsh/lib/machines.zsh ~/.dotfiles/zsh/lib/ +mkdir -p ~/.dotfiles/machines +cp machines/*.zsh ~/.dotfiles/machines/ + +# Notifications +cp zsh/functions/notifications.zsh ~/.dotfiles/zsh/functions/ + +# Profiling +cp bin/dotfiles-profile.sh ~/.dotfiles/bin/ +chmod +x ~/.dotfiles/bin/dotfiles-profile.sh +``` + +### Option 3: Integration + +Add to your `.zshrc` to load new features: + +```zsh +# In ~/.dotfiles/zsh/.zshrc, add after other sources: + +# Load machine-specific configuration +[[ -f "$DOTFILES_DIR/zsh/lib/machines.zsh" ]] && \ + source "$DOTFILES_DIR/zsh/lib/machines.zsh" + +# Load notifications +[[ -f "$DOTFILES_DIR/zsh/functions/notifications.zsh" ]] && \ + source "$DOTFILES_DIR/zsh/functions/notifications.zsh" + +# Load project environments +[[ -f "$DOTFILES_DIR/zsh/functions/project-env.zsh" ]] && \ + source "$DOTFILES_DIR/zsh/functions/project-env.zsh" + +# Load FZF extras +[[ -f "$DOTFILES_DIR/zsh/functions/fzf-extras.zsh" ]] && \ + source "$DOTFILES_DIR/zsh/functions/fzf-extras.zsh" +``` --- -## Features +## Feature Details -| Feature | Description | -|---------|-------------| -| **Dynamic MOTD** | System info on shell start | -| **Two-Line Prompt** | Git status, command timer, update indicator | -| **Command Palette** | Fuzzy launcher (`Ctrl+Space`) | -| **Tmux Workspaces** | Simple templates + tmuxinator integration | -| **Systemd Helpers** | Quick service management | -| **Btrfs/Snapper** | Filesystem health + snapshot management | -| **Secrets Vault** | Encrypted storage (age/gpg) | -| **Password Manager** | LastPass CLI integration | -| **Python Templates** | Project scaffolding (Flask, FastAPI, CLI, etc.) | +### 1. Machine-Specific Configuration + +**Files:** `zsh/lib/machines.zsh`, `machines/*.zsh` + +Automatically loads different settings based on hostname or machine type. + +```bash +# See current machine detection +machine-info + +# Create config for this machine +machine-create + +# Edit machine config +machine-edit + +# List all machine configs +machines +``` + +**Configuration hierarchy:** +1. `dotfiles.conf` (base) +2. `machines/default.zsh` (shared overrides) +3. `machines/type-.zsh` (laptop/desktop/server/virtual) +4. `machines/.zsh` (machine-specific) +5. `~/.zshrc.local` (local, not synced) --- -## Dotfiles Management +### 2. Startup Profiling -| Command | Alias | Description | -|---------|-------|-------------| -| `dotfiles-doctor.sh` | `dfd` | Health check | -| `dotfiles-doctor.sh --fix` | `dffix` | Auto-fix issues | -| `dotfiles-sync.sh push` | `dfpush` | Push changes | -| `dotfiles-sync.sh pull` | `dfpull` | Pull changes | -| `dotfiles-update.sh` | `dfu` | Update dotfiles | -| `dotfiles-vault.sh` | `vault` | Secrets manager | -| `source ~/.zshrc` | `reload` | Reload config | +**File:** `bin/dotfiles-profile.sh` -**Quick Edit:** `v.zshrc`, `v.conf`, `v.alias`, `v.motd` +Measure and optimize shell startup time. + +```bash +dotfiles-profile.sh # Quick timing (5 runs) +dotfiles-profile.sh --detailed # zprof function-level analysis +dotfiles-profile.sh --benchmark # Hyperfine benchmark +dotfiles-profile.sh --compare # Full vs minimal shell +dotfiles-profile.sh --tips # Optimization suggestions +dotfiles-profile.sh --all # Run everything +``` --- -## Systemd Helpers +### 3. Long-Running Command Notifications + +**File:** `zsh/functions/notifications.zsh` + +Get notified when commands taking longer than 60 seconds complete. + +```bash +# Toggle notifications +notify-toggle + +# Test notification +notify-test + +# Show status +notify-status + +# Adjust threshold +df_notify_threshold 120 # 2 minutes +``` + +**Configuration in `dotfiles.conf`:** +```bash +DF_NOTIFY_ENABLED="true" +DF_NOTIFY_THRESHOLD="60" +DF_NOTIFY_METHODS="desktop bell" +``` + +--- + +### 4. Diff & Security Audit + +**File:** `bin/dotfiles-diff.sh` + +Compare configurations and audit for security issues. + +```bash +dotfiles-diff.sh # Show uncommitted changes +dotfiles-diff.sh --installed # Compare installed vs source +dotfiles-diff.sh --symlinks # Verify symlink integrity +dotfiles-diff.sh --secrets # Scan for exposed secrets +dotfiles-diff.sh --permissions # Check file permissions +dotfiles-diff.sh --audit # Full security audit +``` + +--- + +### 5. Project-Local Environments + +**File:** `zsh/functions/project-env.zsh` + +Auto-load project settings when entering directories (like direnv). + +```bash +# Create project env file +project-env create + +# Edit current project's env +project-env edit + +# Show status +project-env status + +# Allow/deny files +project-env allow .dotfiles-local +project-env deny .dotfiles-local +``` + +**Features:** +- Auto-loads `.dotfiles-local`, `.envrc`, or `.env.local` +- Auto-activates Python virtualenvs +- Auto-switches Node versions via `.nvmrc` +- Security prompts for untrusted directories + +--- + +### 6. Enhanced Shell Analytics + +**File:** `bin/dotfiles-analytics.sh` + +Advanced command history analysis. + +```bash +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 # 30-day trends +dotfiles-analytics.sh complexity # Command complexity +dotfiles-analytics.sh tools # Tool usage breakdown +dotfiles-analytics.sh suggestions # Alias suggestions +``` + +--- + +### 7. Testing Framework + +**Files:** `tests/run-tests.zsh`, `tests/test_*.zsh` + +Simple unit testing for shell functions. + +```bash +# Run all tests +./tests/run-tests.zsh + +# Run specific test file +./tests/run-tests.zsh utils + +# Or use alias +dftest +``` + +**Writing tests:** +```zsh +describe "my function" + +it "should do something" +assert_eq "$(my_func)" "expected" + +it "should handle errors" +assert_fail "my_func invalid_arg" +``` + +--- + +### 8. First-Run Experience & Tour + +**File:** `bin/dotfiles-tour.sh` + +Interactive introduction for new users. + +```bash +dotfiles-tour.sh # Interactive tour +dotfiles-tour.sh --quick # Quick reference card +dotfiles-tour.sh --changelog # Recent changes +``` + +--- + +### 9. FZF Extras + +**File:** `zsh/functions/fzf-extras.zsh` + +Additional fuzzy finders. | Command | Description | |---------|-------------| -| `sc ` | `sudo systemctl ` | -| `scr ` | Restart + show status | -| `sce ` | Enable + start | -| `scd ` | Disable + stop | -| `sclog ` | Follow journal logs | -| `sc-failed` | Show failed services | -| `sc-boot` | Boot time analysis | -| `scf` | Interactive manager (fzf) | - -**Aliases:** `scs` (status), `scstart`, `scstop`, `screload`, `jctl`, `jctlf` +| `envf` | Browse environment variables | +| `pathf` | Explore PATH directories | +| `procf` | Process manager | +| `killf` | Fuzzy kill processes | +| `aliasf` | Browse aliases | +| `funcf` | Browse functions | +| `histf` | Enhanced history search | +| `ff` | Find files | +| `fdir` | Find directories | +| `gbf` | Git branch switcher | +| `glogf` | Git commit browser | --- -## Btrfs & Snapper +### 10. Plugin Manager -### Btrfs Commands +**File:** `zsh/lib/plugins.zsh` -| Command | Alias | Description | -|---------|-------|-------------| -| `btrfs-usage` | `btru` | Filesystem usage | -| `btrfs-health` | `btrh` | Quick health check | -| `btrfs-scrub` | - | Start integrity check | -| `btrfs-balance` | - | Balance operation | -| `btrfs-compress` | `btrc` | Compression stats | - -### Snapper Snapshots - -| Command | Alias | Description | -|---------|-------|-------------| -| `snap-create "desc"` | `snap` | Create snapshot | -| `snap-list` | `snapls` | List snapshots | -| `snap-check` | `snapcheck` | Verify limine sync | -| `sys-update` | - | Update with pre/post snapshot | - ---- - -## Tmux Workspaces - -Manage tmux sessions with simple templates or full tmuxinator projects. - -### Quick Commands - -| Command | Alias | Description | -|---------|-------|-------------| -| `tw [template]` | - | Create/attach workspace | -| `tw-list` | `twl` | List active workspaces | -| `tw-templates` | `twt` | Show available templates | -| `tw-save ` | `tws` | Save current layout | -| `twf` | - | Fuzzy search workspaces | - -### Built-in Templates - -| Template | Description | -|----------|-------------| -| `dev` | Editor (50%) + terminal + logs | -| `ops` | 4-pane monitoring grid | -| `ssh-multi` | 4 panes for multi-server | -| `debug` | Main (70%) + helper (30%) | -| `review` | Side-by-side comparison | - -### Tmuxinator Integration - -For complex projects with per-pane commands and startup scripts: +Lightweight plugin management without heavy frameworks. ```bash -# Install -sudo pacman -S tmuxinator +# Install a plugin +plugin install zsh-users/zsh-autosuggestions -# Create project from template -txi-new myproject dev +# List plugins +plugin list -# Edit configuration -txi-edit myproject +# Update all plugins +plugin update -# Start project -txi myproject +# Remove a plugin +plugin remove zsh-autosuggestions + +# Show recommended plugins +plugin recommended + +# Lazy-load a plugin +plugin lazy zsh-nvm nvm node npm ``` -| Command | Alias | Description | -|---------|-------|-------------| -| `txi ` | - | Start/attach project | -| `txi-new [tmpl]` | `txin` | Create project | -| `txi-edit ` | `txie` | Edit YAML config | -| `txi-list` | `txil` | List projects | -| `txif` | - | Fuzzy search projects | +--- -**Templates:** `dev`, `ops`, `web`, `data`, `minimal` +## New Aliases Reference -The `tw` command auto-detects: running session → tmuxinator project → simple template. +| Alias | Command | Description | +|-------|---------|-------------| +| `dfprofile` | `dotfiles-profile.sh` | Startup profiling | +| `dfdiff` | `dotfiles-diff.sh` | Show changes | +| `dfaudit` | `dotfiles-diff.sh --audit` | Security audit | +| `dftour` | `dotfiles-tour.sh` | Interactive tour | +| `dfanalytics` | `dotfiles-analytics.sh` | Enhanced analytics | +| `dftest` | `tests/run-tests.zsh` | Run tests | +| `quickref` | `dotfiles-tour.sh --quick` | Quick reference | +| `profile` | `dotfiles-profile.sh` | Startup profiling | +| `audit` | `dotfiles-diff.sh --audit` | Security audit | +| `tour` | `dotfiles-tour.sh` | Interactive tour | --- -## Command Palette +## Configuration Options -Press **`Ctrl+Space`** for the fuzzy command launcher. - -Searches aliases, functions, history, git commands, bookmarks, and quick actions. - -### Directory Bookmarks - -| Command | Alias | Description | -|---------|-------|-------------| -| `bookmark [path]` | `bm` | Save bookmark | -| `bookmark list` | `bm list` | List bookmarks | -| `jump ` | `j` | Go to bookmark | - ---- - -## Secrets Vault - -Encrypted storage for API keys using `age` or `gpg`. - -| Command | Description | -|---------|-------------| -| `vault init` | Initialize | -| `vault set ` | Store secret | -| `vault get ` | Retrieve secret | -| `vault list` | List keys | -| `vault shell` | Export to environment | - ---- - -## Password Manager (LastPass) - -| Command | Description | -|---------|-------------| -| `pw ` | Search and copy password | -| `pw show ` | Show entry details | -| `pw list` | List all entries | -| `pw gen [len]` | Generate password | -| `pwf` | Fuzzy search (fzf) | - ---- - -## Python Templates - -| Command | Alias | Description | -|---------|-------|-------------| -| `py-new ` | `pynew` | Basic project | -| `py-flask ` | `pyflask` | Flask web app | -| `py-fastapi ` | `pyfast` | FastAPI REST API | -| `py-cli ` | `pycli` | CLI with Click | -| `py-data ` | `pydata` | Data science | -| `venv` | - | Activate virtualenv | - ---- - -## SSH Manager - -| Command | Alias | Description | -|---------|-------|-------------| -| `ssh-save ` | `sshs` | Save profile | -| `ssh-connect ` | `sshc` | Connect (auto tmux) | -| `ssh-list` | `sshl` | List profiles | -| `sshf` | - | Fuzzy search | - ---- - -## Common Aliases - -### Navigation -`..`, `...`, `....`, `~`, `c.` (dotfiles dir) - -### Git -`g`, `gs` (status), `ga` (add), `gc` (commit), `gp` (push), `gl` (pull), `gd` (diff), `gco` (checkout), `glog` - -### Docker -`d`, `dc` (compose), `dps`, `dpa`, `di` (images), `dex` (exec -it) - -### Tools (conditional) -- `ls`/`ll`/`la`/`lt` → `eza` (if installed) -- `cat` → `bat` (if installed) - ---- - -## Zsh Theme - -The `adlee` theme provides: -- Two-line prompt with git branch + dirty indicator -- Command timer for commands >10s (color-coded by duration) -- Package update count indicator -- Root detection (red `#` vs blue `%`) - ---- - -## Configuration - -Edit `~/.dotfiles/dotfiles.conf`: +Add to `dotfiles.conf`: ```bash -# Display -DF_WIDTH="74" -MOTD_STYLE="compact" # compact, mini, full, none +# === NEW: Notification Settings === +DF_NOTIFY_ENABLED="true" +DF_NOTIFY_THRESHOLD="60" +DF_NOTIFY_METHODS="desktop bell" +DF_NOTIFY_SOUND="/usr/share/sounds/freedesktop/stereo/complete.oga" +DF_NOTIFY_ONLY_FAILURES="false" -# Features -ENABLE_SMART_SUGGESTIONS="true" -ENABLE_COMMAND_PALETTE="true" +# === NEW: Project Environment === +DF_PROJECT_ENV_ENABLED="true" +DF_PROJECT_ENV_FILES=".dotfiles-local .envrc .env.local" +DF_PROJECT_ENV_TRUSTED_DIRS="$HOME/projects $HOME/work" +DF_PROJECT_AUTO_VENV="true" +DF_PROJECT_AUTO_NVM="true" -# Tmuxinator -TMUXINATOR_ENABLED="auto" -TW_PREFER_TMUXINATOR="true" +# === NEW: Plugin Manager === +DF_PLUGIN_DIR="$HOME/.dotfiles/zsh/plugins" ``` -### Local Overrides - -Create `~/.zshrc.local` for machine-specific settings. - --- -## Repository Structure +## Verification + +After installation, verify everything works: + +```bash +# Health check +dfd + +# Run tests +dftest + +# Profile startup +dfprofile + +# Security audit +dfaudit + +# Take the tour +dftour +``` + +--- + +## File Structure ``` -~/.dotfiles/ -├── install.sh # Installer -├── dotfiles.conf # Configuration -├── bin/ # Scripts → ~/.local/bin +dotfiles-improvements/ +├── bin/ +│ ├── dotfiles-analytics.sh # Enhanced history analytics +│ ├── dotfiles-diff.sh # Diff and security audit +│ ├── dotfiles-profile.sh # Startup profiling +│ └── dotfiles-tour.sh # First-run experience +├── machines/ +│ ├── default.zsh # Shared machine config +│ ├── type-laptop.zsh # Laptop-specific +│ └── type-server.zsh # Server-specific +├── tests/ +│ ├── run-tests.zsh # Test runner +│ ├── test_config.zsh # Config tests +│ └── test_utils.zsh # Utils tests ├── zsh/ -│ ├── .zshrc -│ ├── aliases.zsh -│ ├── lib/ # colors, config, utils, bootstrap -│ ├── themes/adlee.zsh-theme -│ └── functions/ # Feature modules -├── vim/.vimrc -├── tmux/.tmux.conf -├── espanso/ # Text expansion -└── .tmux-templates/ # Workspace layouts +│ ├── aliases-extended.zsh # Extended aliases +│ ├── functions/ +│ │ ├── fzf-extras.zsh # Additional fzf utilities +│ │ ├── notifications.zsh # Command notifications +│ │ └── project-env.zsh # Project environments +│ └── lib/ +│ ├── machines.zsh # Machine detection +│ └── plugins.zsh # Plugin manager +└── README.md # This file ``` - ---- - -## License - -MIT – See [LICENSE](LICENSE) - -**Author:** Aaron D. Lee -**Repository:** https://github.com/adlee-was-taken/dotfiles diff --git a/bin/dotfiles-analytics.sh b/bin/dotfiles-analytics.sh new file mode 100755 index 0000000..c1d53c9 --- /dev/null +++ b/bin/dotfiles-analytics.sh @@ -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/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= 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]' || 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 "$@" diff --git a/bin/dotfiles-diff.sh b/bin/dotfiles-diff.sh new file mode 100755 index 0000000..06d1346 --- /dev/null +++ b/bin/dotfiles-diff.sh @@ -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 "$@" diff --git a/bin/dotfiles-profile.sh b/bin/dotfiles-profile.sh new file mode 100755 index 0000000..af4ccb2 --- /dev/null +++ b/bin/dotfiles-profile.sh @@ -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 "$@" diff --git a/bin/dotfiles-tour.sh b/bin/dotfiles-tour.sh new file mode 100755 index 0000000..5f5ce34 --- /dev/null +++ b/bin/dotfiles-tour.sh @@ -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 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 Create workspace twl List │ +│ twf Fuzzy search tws Save layout │ +├──────────────────────────────────────────────────────────────┤ +│ SYSTEM │ +│ sc systemctl scf Interactive │ +│ scr 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 "$@" diff --git a/machines/default.zsh b/machines/default.zsh new file mode 100644 index 0000000..b83a162 --- /dev/null +++ b/machines/default.zsh @@ -0,0 +1,58 @@ +# ============================================================================ +# Default Machine Configuration +# ============================================================================ +# This file is loaded on ALL machines before hostname-specific configs. +# Use it for settings that should be shared across all your machines. +# +# Load order: +# 1. dotfiles.conf (base config) +# 2. machines/default.zsh (this file) +# 3. machines/type-.zsh (laptop, desktop, server, virtual) +# 4. machines/.zsh (machine-specific) +# 5. ~/.zshrc.local (local overrides, not synced) +# ============================================================================ + +# ============================================================================ +# Shared Settings +# ============================================================================ + +# Uncomment and modify settings you want on all machines + +# --- Display --- +# DF_WIDTH="74" + +# --- Features --- +# ENABLE_SMART_SUGGESTIONS="true" +# ENABLE_COMMAND_PALETTE="true" + +# --- Notification Settings --- +# DF_NOTIFY_ENABLED="true" +# DF_NOTIFY_THRESHOLD="60" + +# --- Project Environment --- +# DF_PROJECT_ENV_ENABLED="true" +# DF_PROJECT_AUTO_VENV="true" + +# ============================================================================ +# Shared Aliases +# ============================================================================ + +# Add aliases that should exist on all machines +# alias mycompany='cd ~/work/mycompany' + +# ============================================================================ +# Shared Environment +# ============================================================================ + +# Environment variables for all machines +# export EDITOR="nvim" +# export BROWSER="firefox" + +# ============================================================================ +# Shared Functions +# ============================================================================ + +# Functions available on all machines +# myfunction() { +# echo "This works everywhere" +# } diff --git a/machines/type-laptop.zsh b/machines/type-laptop.zsh new file mode 100644 index 0000000..6d39995 --- /dev/null +++ b/machines/type-laptop.zsh @@ -0,0 +1,31 @@ +# ============================================================================ +# Laptop Machine Type Configuration +# ============================================================================ +# Loaded on machines detected as laptops (has battery). +# ============================================================================ + +# --- Power-aware settings --- +# Reduce resource usage on battery + +# Shorter MOTD on laptops (faster) +# MOTD_STYLE="mini" + +# --- Battery monitoring alias --- +alias battery='cat /sys/class/power_supply/BAT0/capacity 2>/dev/null && echo "%" || echo "No battery"' +alias power='cat /sys/class/power_supply/BAT0/status 2>/dev/null || echo "Unknown"' + +# --- Brightness control (if available) --- +if command -v brightnessctl &>/dev/null; then + alias bright='brightnessctl set' + alias brightness='brightnessctl get' +fi + +# --- WiFi helpers --- +if command -v nmcli &>/dev/null; then + alias wifi='nmcli device wifi list' + alias wifi-connect='nmcli device wifi connect' +fi + +# --- Suspend/hibernate helpers --- +alias suspend='systemctl suspend' +alias hibernate='systemctl hibernate' diff --git a/machines/type-server.zsh b/machines/type-server.zsh new file mode 100644 index 0000000..d2eea9b --- /dev/null +++ b/machines/type-server.zsh @@ -0,0 +1,36 @@ +# ============================================================================ +# Server Machine Type Configuration +# ============================================================================ +# Loaded on machines detected as servers. +# ============================================================================ + +# --- Minimal MOTD (servers don't need fancy displays) --- +MOTD_STYLE="mini" + +# --- Disable notifications (no desktop on servers) --- +DF_NOTIFY_ENABLED="false" + +# --- Server monitoring aliases --- +alias ports='ss -tulpn' +alias listening='ss -tulpn | grep LISTEN' +alias connections='ss -tan | grep ESTAB | wc -l' + +# --- Log watching --- +alias syslog='sudo tail -f /var/log/syslog 2>/dev/null || sudo journalctl -f' +alias authlog='sudo tail -f /var/log/auth.log 2>/dev/null || sudo journalctl -f -u sshd' + +# --- Docker shortcuts (servers often run containers) --- +if command -v docker &>/dev/null; then + alias dstats='docker stats --no-stream' + alias dclean='docker system prune -af' + alias dlogs='docker logs -f' +fi + +# --- Quick system checks --- +alias diskspace='df -h | grep -v tmpfs | grep -v loop' +alias meminfo='free -h' +alias cpuinfo='lscpu | grep -E "Model name|Socket|Core|Thread"' + +# --- Security --- +alias failed-logins='sudo journalctl -u sshd | grep -i "failed\|invalid"' +alias active-users='who' diff --git a/tests/run-tests.zsh b/tests/run-tests.zsh new file mode 100755 index 0000000..74177cd --- /dev/null +++ b/tests/run-tests.zsh @@ -0,0 +1,241 @@ +#!/usr/bin/env zsh +# ============================================================================ +# Dotfiles Test Framework +# ============================================================================ +# Simple unit testing for shell functions and scripts. +# +# Usage: +# ./tests/run-tests.zsh # Run all tests +# ./tests/run-tests.zsh test_utils # Run specific test file +# ============================================================================ + +set -e + +# ============================================================================ +# Configuration +# ============================================================================ + +SCRIPT_DIR="${0:A:h}" +DOTFILES_DIR="${SCRIPT_DIR:h}" +TESTS_DIR="$SCRIPT_DIR" + +# Colors +RED=$'\033[0;31m' +GREEN=$'\033[0;32m' +YELLOW=$'\033[1;33m' +CYAN=$'\033[0;36m' +NC=$'\033[0m' + +# Counters +TESTS_RUN=0 +TESTS_PASSED=0 +TESTS_FAILED=0 +CURRENT_TEST="" + +# ============================================================================ +# Test Framework Functions +# ============================================================================ + +# Start a test group +describe() { + echo "" + echo -e "${CYAN}▶ $1${NC}" +} + +# Run a single test +it() { + CURRENT_TEST="$1" + ((TESTS_RUN++)) +} + +# Assert equality +assert_eq() { + local actual="$1" + local expected="$2" + local message="${3:-Values should be equal}" + + if [[ "$actual" == "$expected" ]]; then + echo -e " ${GREEN}✓${NC} $CURRENT_TEST" + ((TESTS_PASSED++)) + else + echo -e " ${RED}✗${NC} $CURRENT_TEST" + echo -e " ${RED}Expected:${NC} $expected" + echo -e " ${RED}Actual:${NC} $actual" + ((TESTS_FAILED++)) + fi +} + +# Assert not equal +assert_ne() { + local actual="$1" + local not_expected="$2" + + if [[ "$actual" != "$not_expected" ]]; then + echo -e " ${GREEN}✓${NC} $CURRENT_TEST" + ((TESTS_PASSED++)) + else + echo -e " ${RED}✗${NC} $CURRENT_TEST" + echo -e " ${RED}Should not equal:${NC} $not_expected" + ((TESTS_FAILED++)) + fi +} + +# Assert command succeeds +assert_success() { + local cmd="$1" + + if eval "$cmd" &>/dev/null; then + echo -e " ${GREEN}✓${NC} $CURRENT_TEST" + ((TESTS_PASSED++)) + else + echo -e " ${RED}✗${NC} $CURRENT_TEST" + echo -e " ${RED}Command failed:${NC} $cmd" + ((TESTS_FAILED++)) + fi +} + +# Assert command fails +assert_fail() { + local cmd="$1" + + if ! eval "$cmd" &>/dev/null; then + echo -e " ${GREEN}✓${NC} $CURRENT_TEST" + ((TESTS_PASSED++)) + else + echo -e " ${RED}✗${NC} $CURRENT_TEST" + echo -e " ${RED}Expected failure but succeeded:${NC} $cmd" + ((TESTS_FAILED++)) + fi +} + +# Assert string contains +assert_contains() { + local haystack="$1" + local needle="$2" + + if [[ "$haystack" == *"$needle"* ]]; then + echo -e " ${GREEN}✓${NC} $CURRENT_TEST" + ((TESTS_PASSED++)) + else + echo -e " ${RED}✗${NC} $CURRENT_TEST" + echo -e " ${RED}String should contain:${NC} $needle" + ((TESTS_FAILED++)) + fi +} + +# Assert file exists +assert_file_exists() { + local file="$1" + + if [[ -f "$file" ]]; then + echo -e " ${GREEN}✓${NC} $CURRENT_TEST" + ((TESTS_PASSED++)) + else + echo -e " ${RED}✗${NC} $CURRENT_TEST" + echo -e " ${RED}File not found:${NC} $file" + ((TESTS_FAILED++)) + fi +} + +# Assert directory exists +assert_dir_exists() { + local dir="$1" + + if [[ -d "$dir" ]]; then + echo -e " ${GREEN}✓${NC} $CURRENT_TEST" + ((TESTS_PASSED++)) + else + echo -e " ${RED}✗${NC} $CURRENT_TEST" + echo -e " ${RED}Directory not found:${NC} $dir" + ((TESTS_FAILED++)) + fi +} + +# Assert command exists +assert_cmd_exists() { + local cmd="$1" + + if command -v "$cmd" &>/dev/null; then + echo -e " ${GREEN}✓${NC} $CURRENT_TEST" + ((TESTS_PASSED++)) + else + echo -e " ${RED}✗${NC} $CURRENT_TEST" + echo -e " ${RED}Command not found:${NC} $cmd" + ((TESTS_FAILED++)) + fi +} + +# Skip a test +skip() { + local reason="${1:-No reason given}" + echo -e " ${YELLOW}○${NC} $CURRENT_TEST (SKIPPED: $reason)" +} + +# ============================================================================ +# Test Runner +# ============================================================================ + +run_test_file() { + local test_file="$1" + source "$test_file" +} + +print_summary() { + echo "" + echo "─────────────────────────────────────────" + echo -e "Tests: ${CYAN}$TESTS_RUN${NC}" + echo -e "Passed: ${GREEN}$TESTS_PASSED${NC}" + echo -e "Failed: ${RED}$TESTS_FAILED${NC}" + echo "─────────────────────────────────────────" + + if (( TESTS_FAILED > 0 )); then + echo -e "${RED}FAILED${NC}" + return 1 + else + echo -e "${GREEN}PASSED${NC}" + return 0 + fi +} + +# ============================================================================ +# Main +# ============================================================================ + +main() { + echo "╔═══════════════════════════════════════╗" + echo "║ Dotfiles Test Suite ║" + echo "╚═══════════════════════════════════════╝" + + # Source the libraries we're testing + source "$DOTFILES_DIR/zsh/lib/bootstrap.zsh" 2>/dev/null || true + + local specific_test="$1" + + if [[ -n "$specific_test" ]]; then + # Run specific test file + if [[ -f "$TESTS_DIR/${specific_test}.zsh" ]]; then + run_test_file "$TESTS_DIR/${specific_test}.zsh" + elif [[ -f "$TESTS_DIR/test_${specific_test}.zsh" ]]; then + run_test_file "$TESTS_DIR/test_${specific_test}.zsh" + else + echo "Test file not found: $specific_test" + exit 1 + fi + else + # Run all test files + for test_file in "$TESTS_DIR"/test_*.zsh(N); do + [[ -f "$test_file" ]] || continue + echo "" + echo -e "${YELLOW}Running: $(basename "$test_file")${NC}" + run_test_file "$test_file" + done + fi + + print_summary +} + +# Export functions for test files +export -f describe it assert_eq assert_ne assert_success assert_fail +export -f assert_contains assert_file_exists assert_dir_exists assert_cmd_exists skip + +main "$@" diff --git a/tests/test_config.zsh b/tests/test_config.zsh new file mode 100755 index 0000000..65f0a79 --- /dev/null +++ b/tests/test_config.zsh @@ -0,0 +1,94 @@ +#!/usr/bin/env zsh +# ============================================================================ +# Tests for zsh/lib/config.zsh +# ============================================================================ + +# Source config if not already loaded +source "${DOTFILES_DIR:-$HOME/.dotfiles}/zsh/lib/config.zsh" 2>/dev/null + +# ============================================================================ +# Tests +# ============================================================================ + +describe "Core configuration variables" + +it "should have DOTFILES_VERSION defined" +assert_ne "${DOTFILES_VERSION:-}" "" + +it "should have DOTFILES_DIR defined" +assert_ne "${DOTFILES_DIR:-}" "" + +it "should have DOTFILES_BRANCH defined" +assert_ne "${DOTFILES_BRANCH:-}" "" + +# ============================================================================ + +describe "Display configuration" + +it "should have DF_WIDTH defined" +assert_ne "${DF_WIDTH:-}" "" + +it "should have DF_WIDTH as a number" +[[ "${DF_WIDTH:-66}" =~ ^[0-9]+$ ]] && assert_success "true" || assert_fail "DF_WIDTH not a number" + +it "should have MOTD_STYLE defined" +assert_ne "${MOTD_STYLE:-}" "" + +it "should have ENABLE_MOTD defined" +assert_ne "${ENABLE_MOTD:-}" "" + +# ============================================================================ + +describe "Theme configuration" + +it "should have ZSH_THEME_NAME defined" +assert_ne "${ZSH_THEME_NAME:-}" "" + +it "should have THEME_TIMER_THRESHOLD defined" +assert_ne "${THEME_TIMER_THRESHOLD:-}" "" + +# ============================================================================ + +describe "Feature toggles" + +it "should have ENABLE_SMART_SUGGESTIONS defined" +assert_ne "${ENABLE_SMART_SUGGESTIONS:-}" "" + +it "should have ENABLE_COMMAND_PALETTE defined" +assert_ne "${ENABLE_COMMAND_PALETTE:-}" "" + +it "should have ENABLE_VAULT defined" +assert_ne "${ENABLE_VAULT:-}" "" + +# ============================================================================ + +describe "Path configuration" + +it "should have valid DOTFILES_DIR path" +assert_dir_exists "${DOTFILES_DIR:-$HOME/.dotfiles}" + +it "should have dotfiles.conf in DOTFILES_DIR" +if [[ -d "${DOTFILES_DIR:-$HOME/.dotfiles}" ]]; then + assert_file_exists "${DOTFILES_DIR:-$HOME/.dotfiles}/dotfiles.conf" +else + skip "DOTFILES_DIR not found" +fi + +# ============================================================================ + +describe "df_config helper function" + +it "should have df_config function defined" +if typeset -f df_config &>/dev/null; then + assert_success "true" +else + skip "df_config not defined in this version" +fi + +it "should return default for undefined variable" +if typeset -f df_config &>/dev/null; then + local result=$(df_config "UNDEFINED_VAR_12345" "default_value") + assert_eq "$result" "default_value" +else + skip "df_config not defined" +fi diff --git a/tests/test_utils.zsh b/tests/test_utils.zsh new file mode 100755 index 0000000..3a1b4cb --- /dev/null +++ b/tests/test_utils.zsh @@ -0,0 +1,111 @@ +#!/usr/bin/env zsh +# ============================================================================ +# Tests for zsh/lib/utils.zsh +# ============================================================================ + +# Source utils if not already loaded +source "${DOTFILES_DIR:-$HOME/.dotfiles}/zsh/lib/utils.zsh" 2>/dev/null + +# ============================================================================ +# Tests +# ============================================================================ + +describe "df_cmd_exists" + +it "should return true for existing command (ls)" +assert_success "df_cmd_exists ls" + +it "should return false for non-existent command" +assert_fail "df_cmd_exists this_command_does_not_exist_12345" + +it "should work with common tools" +assert_success "df_cmd_exists git" + +# ============================================================================ + +describe "df_print functions" + +it "should have df_print_success defined" +assert_success "typeset -f df_print_success" + +it "should have df_print_error defined" +assert_success "typeset -f df_print_error" + +it "should have df_print_warning defined" +assert_success "typeset -f df_print_warning" + +it "should have df_print_step defined" +assert_success "typeset -f df_print_step" + +# ============================================================================ + +describe "df_in_git_repo" + +it "should detect git repository in dotfiles dir" +( + cd "${DOTFILES_DIR:-$HOME/.dotfiles}" + if [[ -d .git ]]; then + assert_success "df_in_git_repo" + else + skip "Not a git repo" + fi +) + +it "should return false in /tmp" +( + cd /tmp + assert_fail "df_in_git_repo" +) + +# ============================================================================ + +describe "df_ensure_dir" + +it "should create directory if it doesn't exist" +local test_dir="/tmp/dotfiles_test_$$" +df_ensure_dir "$test_dir" +assert_dir_exists "$test_dir" +rmdir "$test_dir" 2>/dev/null + +it "should not fail if directory exists" +df_ensure_dir "/tmp" +assert_success "true" + +# ============================================================================ + +describe "_df_hline" + +it "should create a line of specified width" +local line=$(_df_hline "=" 10) +assert_eq "${#line}" "10" + +it "should use specified character" +local line=$(_df_hline "-" 5) +assert_eq "$line" "-----" + +# ============================================================================ + +describe "Color variables" + +it "should have DF_GREEN defined" +assert_ne "$DF_GREEN" "" + +it "should have DF_RED defined" +assert_ne "$DF_RED" "" + +it "should have DF_NC (reset) defined" +assert_ne "$DF_NC" "" + +it "should have DF_CYAN defined" +assert_ne "$DF_CYAN" "" + +# ============================================================================ + +describe "Configuration variables" + +it "should have DOTFILES_DIR defined" +assert_ne "${DOTFILES_DIR:-}" "" + +it "should have DF_WIDTH defined with reasonable value" +local width="${DF_WIDTH:-66}" +(( width >= 40 && width <= 120 )) && assert_success "true" || assert_fail "DF_WIDTH out of range" diff --git a/zsh/aliases-extended.zsh b/zsh/aliases-extended.zsh new file mode 100644 index 0000000..b8adeaf --- /dev/null +++ b/zsh/aliases-extended.zsh @@ -0,0 +1,206 @@ +# ============================================================================ +# Dotfiles Command Aliases - Extended Version +# ============================================================================ +# Includes all original aliases plus new improvement aliases. +# ============================================================================ + +# Dotfiles directory +_df_dir="${DOTFILES_DIR:-$HOME/.dotfiles}" +_df_bin="$_df_dir/bin" + +# Helper to run dotfiles scripts +_df_run() { + local script="$1" + shift + if [[ -x "$_df_bin/$script" ]]; then + "$_df_bin/$script" "$@" + elif command -v "$script" &>/dev/null; then + "$script" "$@" + else + echo "Error: $script not found" >&2 + return 1 + fi +} + +# ============================================================================ +# Quality of Life Aliases +# ============================================================================ + +alias hist="history" +alias cls="clear" +alias q="exit" + +# ============================================================================ +# Core Dotfiles Commands +# ============================================================================ + +alias dfdir='cd $HOME/.dotfiles' +alias c.='cd $HOME/.dotfiles' + +# Doctor - health check +dfd() { _df_run dotfiles-doctor.sh "$@"; } +doctor() { _df_run dotfiles-doctor.sh "$@"; } +dffix() { _df_run dotfiles-doctor.sh --fix "$@"; } + +# Sync +dfs() { _df_run dotfiles-sync.sh "$@"; } +dfpush() { _df_run dotfiles-sync.sh push "${1:-Dotfiles update $(date '+%Y-%m-%d %H:%M')}"; } +dfpull() { _df_run dotfiles-sync.sh pull "$@"; } +dfstatus() { _df_run dotfiles-sync.sh status "$@"; } + +# Update +dfu() { _df_run dotfiles-update.sh "$@"; } +dfupdate() { _df_run dotfiles-update.sh "$@"; } + +# Version +dfv() { _df_run dotfiles-version.sh "$@"; } +dfversion() { _df_run dotfiles-version.sh "$@"; } + +# Stats / Analytics +dfstats() { _df_run dotfiles-stats.sh "$@"; } +dfanalytics() { _df_run dotfiles-analytics.sh "$@"; } + +# Vault +vault() { _df_run dotfiles-vault.sh "$@"; } +vls() { _df_run dotfiles-vault.sh list "$@"; } +vget() { _df_run dotfiles-vault.sh get "$@"; } +vset() { _df_run dotfiles-vault.sh set "$@"; } + +# Compile +dfcompile() { _df_run dotfiles-compile.sh "$@"; } + +# ============================================================================ +# NEW: Profile & Performance +# ============================================================================ + +dfprofile() { _df_run dotfiles-profile.sh "$@"; } +alias profile='dfprofile' +alias startup-time='dfprofile --quick' + +# ============================================================================ +# NEW: Diff & Audit +# ============================================================================ + +dfdiff() { _df_run dotfiles-diff.sh "$@"; } +dfaudit() { _df_run dotfiles-diff.sh --audit "$@"; } +alias audit='dfaudit' + +# ============================================================================ +# NEW: Tour & First-Run +# ============================================================================ + +dftour() { _df_run dotfiles-tour.sh "$@"; } +alias tour='dftour' +alias quickref='dftour --quick' + +# ============================================================================ +# Quick Edit Aliases +# ============================================================================ + +alias v.zshrc='${EDITOR:-vim} ~/.zshrc' +alias v.conf='${EDITOR:-vim} ~/.dotfiles/dotfiles.conf' +alias v.edit='cd ~/.dotfiles && ${EDITOR:-vim} .' +alias v.alias='${EDITOR:-vim} ~/.dotfiles/zsh/aliases.zsh' +alias v.motd='${EDITOR:-vim} ~/.dotfiles/zsh/functions/motd.zsh' +alias v.theme='${EDITOR:-vim} ~/.dotfiles/zsh/themes/adlee.zsh-theme' + +# NEW: Edit machine config +alias v.machine='${EDITOR:-vim} ~/.dotfiles/machines/${DF_HOSTNAME:-$(hostname -s)}.zsh' + +# ============================================================================ +# Reload Aliases +# ============================================================================ + +alias reload='source ~/.zshrc' +alias rl='source ~/.zshrc' + +# ============================================================================ +# Dotfiles CLI +# ============================================================================ + +dotfiles-cli() { + case "${1:-help}" in + doctor|doc|d) shift; _df_run dotfiles-doctor.sh "$@" ;; + sync|s) shift; _df_run dotfiles-sync.sh "$@" ;; + update|up|u) shift; _df_run dotfiles-update.sh "$@" ;; + version|ver|v) shift; _df_run dotfiles-version.sh "$@" ;; + stats|st) shift; _df_run dotfiles-stats.sh "$@" ;; + analytics|an) shift; _df_run dotfiles-analytics.sh "$@" ;; + vault|vlt) shift; _df_run dotfiles-vault.sh "$@" ;; + compile|comp) shift; _df_run dotfiles-compile.sh "$@" ;; + profile|prof) shift; _df_run dotfiles-profile.sh "$@" ;; + diff|df) shift; _df_run dotfiles-diff.sh "$@" ;; + audit|aud) shift; _df_run dotfiles-diff.sh --audit "$@" ;; + tour|t) shift; _df_run dotfiles-tour.sh "$@" ;; + test) shift; zsh ~/.dotfiles/tests/run-tests.zsh "$@" ;; + edit|e) cd ~/.dotfiles && ${EDITOR:-vim} . ;; + cd) cd ~/.dotfiles ;; + help|--help|-h|*) + cat << 'EOF' +Dotfiles CLI - Extended + +Usage: dotfiles-cli [args] + +Core Commands: + doctor, d Health check (--fix to repair) + sync, s Sync dotfiles across machines + update, u Pull latest and reinstall + version, v Show version info + stats, st Basic shell analytics + vault, vlt Secrets management + compile Compile zsh files for speed + +New Commands: + analytics, an Enhanced shell analytics + profile, prof Startup time profiling + diff, df Show changes and compare + audit, aud Security audit + tour, t Interactive tour / help + test Run test suite + +Navigation: + edit, e Open dotfiles in editor + cd Change to dotfiles directory + +Aliases: dfd, dffix, dfs, dfpush, dfpull, dfu, dfv, dfstats, vault + dfprofile, dfdiff, dfaudit, dftour + +EOF + ;; + esac +} + +alias dfc='dotfiles-cli' + +# ============================================================================ +# System Utilities +# ============================================================================ + +# Use glow for markdown +alias glow='glow -p' +less() { + if command -v glow &>/dev/null && [[ $# -eq 1 && "$1" == *.md ]]; then + glow -p "$1" + else + command less "$@" + fi +} + +# Arch system upgrade with snapper +sys-update() { + local update_date=$(date +"%Y-%m-%d %H:%M") + if command -v snapper &>/dev/null; then + sudo snapper -c root create --description "System Update ${update_date}" --command "sudo pacman -Syu" + else + sudo pacman -Syu + fi + # Update package count for prompt + command -v checkupdates &>/dev/null && export UPDATE_PKG_COUNT=$(checkupdates 2>/dev/null | wc -l) +} + +# ============================================================================ +# Testing +# ============================================================================ + +alias dftest='zsh ~/.dotfiles/tests/run-tests.zsh' +alias test-dotfiles='dftest' diff --git a/zsh/functions/fzf-extras.zsh b/zsh/functions/fzf-extras.zsh new file mode 100644 index 0000000..f90baac --- /dev/null +++ b/zsh/functions/fzf-extras.zsh @@ -0,0 +1,500 @@ +# ============================================================================ +# FZF-Powered Utilities +# ============================================================================ +# Additional fuzzy finders for various system exploration tasks. +# +# Features: +# - envf: Environment variable browser +# - pathf: PATH explorer +# - procf: Process manager +# - aliasf: Alias browser +# - funcf: Function browser +# - histf: Enhanced history search +# ============================================================================ + +# Prevent double-sourcing +[[ -n "$_DF_FZF_EXTRAS_LOADED" ]] && return 0 +typeset -g _DF_FZF_EXTRAS_LOADED=1 + +# Source utils +source "${0:A:h}/../lib/utils.zsh" 2>/dev/null || \ +source "$HOME/.dotfiles/zsh/lib/utils.zsh" 2>/dev/null + +# ============================================================================ +# Check FZF +# ============================================================================ + +_fzf_check() { + if ! command -v fzf &>/dev/null; then + df_print_error "fzf not installed" + df_print_info "Install: sudo pacman -S fzf" + return 1 + fi + return 0 +} + +# Common fzf options +_fzf_common_opts() { + echo "--height=60% --layout=reverse --border=rounded --info=inline" +} + +# ============================================================================ +# Environment Variable Browser +# ============================================================================ + +envf() { + _fzf_check || return 1 + + local selected=$(env | sort | fzf $(_fzf_common_opts) \ + --prompt="Env > " \ + --preview='echo {} | cut -d= -f1 | xargs -I{} bash -c "echo -e \"Variable: {}\n\nValue:\n\"; printenv {}"' \ + --preview-window=right:50%:wrap \ + --header="Enter: copy value | Ctrl-E: edit | Ctrl-U: unset") + + [[ -z "$selected" ]] && return + + local var_name="${selected%%=*}" + local var_value="${selected#*=}" + + echo "" + echo -e "${DF_CYAN}$var_name${DF_NC}=$var_value" + echo "" + + # Copy to clipboard if available + if command -v wl-copy &>/dev/null; then + echo -n "$var_value" | wl-copy + df_print_success "Value copied to clipboard" + elif command -v xclip &>/dev/null; then + echo -n "$var_value" | xclip -selection clipboard + df_print_success "Value copied to clipboard" + fi +} + +# Set/edit environment variable interactively +env-set() { + local var_name="$1" + + if [[ -z "$var_name" ]]; then + _fzf_check || return 1 + var_name=$(env | cut -d= -f1 | sort | fzf $(_fzf_common_opts) \ + --prompt="Select var to edit > " \ + --header="Select existing variable or type new name") + [[ -z "$var_name" ]] && return + fi + + local current_value="${(P)var_name}" + + echo "Variable: $var_name" + echo "Current: ${current_value:-(not set)}" + echo "" + read -r "new_value?New value: " + + if [[ -n "$new_value" ]]; then + export "$var_name"="$new_value" + df_print_success "Set $var_name=$new_value" + fi +} + +# ============================================================================ +# PATH Explorer +# ============================================================================ + +pathf() { + _fzf_check || return 1 + + local selected=$(echo "$PATH" | tr ':' '\n' | nl -ba | \ + fzf $(_fzf_common_opts) \ + --prompt="PATH > " \ + --preview='dir=$(echo {} | awk "{print \$2}"); + if [[ -d "$dir" ]]; then + echo "Directory: $dir" + echo "" + echo "Executables:" + ls -1 "$dir" 2>/dev/null | head -30 + count=$(ls -1 "$dir" 2>/dev/null | wc -l) + [[ $count -gt 30 ]] && echo "... and $((count-30)) more" + else + echo "Directory not found: $dir" + fi' \ + --preview-window=right:50% \ + --header="PATH entries (in order)") + + [[ -z "$selected" ]] && return + + local dir=$(echo "$selected" | awk '{print $2}') + + echo "" + df_print_section "Directory: $dir" + + if [[ -d "$dir" ]]; then + ls -la "$dir" | head -20 + else + df_print_warning "Directory does not exist" + fi +} + +# Add to PATH interactively +path-add() { + local dir="${1:-$PWD}" + + if [[ ! -d "$dir" ]]; then + df_print_error "Not a directory: $dir" + return 1 + fi + + dir=$(realpath "$dir") + + if [[ ":$PATH:" == *":$dir:"* ]]; then + df_print_warning "Already in PATH: $dir" + return 0 + fi + + echo "Add to PATH: $dir" + echo "" + echo "1) Prepend (higher priority)" + echo "2) Append (lower priority)" + echo "3) Cancel" + echo "" + + read -k1 "choice?Choice [1]: " + echo "" + + case "${choice:-1}" in + 1) + export PATH="$dir:$PATH" + df_print_success "Prepended to PATH" + ;; + 2) + export PATH="$PATH:$dir" + df_print_success "Appended to PATH" + ;; + *) + echo "Cancelled" + ;; + esac +} + +# ============================================================================ +# Process Manager +# ============================================================================ + +procf() { + _fzf_check || return 1 + + local selected=$(ps aux --sort=-%mem | \ + fzf $(_fzf_common_opts) \ + --prompt="Process > " \ + --header-lines=1 \ + --preview='pid=$(echo {} | awk "{print \$2}"); + echo "=== Process Details ===" + ps -p $pid -o pid,ppid,user,%cpu,%mem,stat,start,time,cmd 2>/dev/null + echo "" + echo "=== Open Files (first 10) ===" + sudo lsof -p $pid 2>/dev/null | head -10 || echo "(requires sudo)" + echo "" + echo "=== Environment (first 10) ===" + sudo cat /proc/$pid/environ 2>/dev/null | tr "\0" "\n" | head -10 || echo "(requires sudo)"' \ + --preview-window=right:50%:wrap \ + --header="Process list | Enter: details | Ctrl-K: kill") + + [[ -z "$selected" ]] && return + + local pid=$(echo "$selected" | awk '{print $2}') + local cmd=$(echo "$selected" | awk '{for(i=11;i<=NF;i++) printf $i" "; print ""}') + + echo "" + df_print_section "Selected Process" + echo " PID: $pid" + echo " CMD: $cmd" + echo "" + echo "Actions:" + echo " 1) Show details" + echo " 2) Send SIGTERM (graceful)" + echo " 3) Send SIGKILL (force)" + echo " 4) Send SIGHUP (reload)" + echo " 5) Cancel" + echo "" + + read -k1 "action?Action [1]: " + echo "" + + case "${action:-1}" in + 1) + ps -p "$pid" -f + ;; + 2) + df_print_step "Sending SIGTERM to $pid..." + kill -TERM "$pid" 2>/dev/null && df_print_success "Signal sent" || df_print_error "Failed (try sudo)" + ;; + 3) + df_print_step "Sending SIGKILL to $pid..." + kill -KILL "$pid" 2>/dev/null && df_print_success "Signal sent" || df_print_error "Failed (try sudo)" + ;; + 4) + df_print_step "Sending SIGHUP to $pid..." + kill -HUP "$pid" 2>/dev/null && df_print_success "Signal sent" || df_print_error "Failed (try sudo)" + ;; + *) + echo "Cancelled" + ;; + esac +} + +# Quick kill by name +killf() { + _fzf_check || return 1 + + local selected=$(ps aux | grep -v "grep" | \ + fzf $(_fzf_common_opts) \ + --prompt="Kill > " \ + --header-lines=1 \ + --multi \ + --header="Select process(es) to kill (Tab to select multiple)") + + [[ -z "$selected" ]] && return + + echo "$selected" | while read -r line; do + local pid=$(echo "$line" | awk '{print $2}') + local cmd=$(echo "$line" | awk '{for(i=11;i<=NF;i++) printf $i" "; print ""}') + + df_print_step "Killing PID $pid ($cmd)" + kill "$pid" 2>/dev/null && df_print_success "Killed" || df_print_error "Failed" + done +} + +# ============================================================================ +# Alias Browser +# ============================================================================ + +aliasf() { + _fzf_check || return 1 + + local selected=$(alias | sed "s/^alias //" | sort | \ + fzf $(_fzf_common_opts) \ + --prompt="Alias > " \ + --preview='name=$(echo {} | cut -d= -f1); + cmd=$(echo {} | cut -d= -f2- | sed "s/^'\''//;s/'\''$//"); + echo "Alias: $name" + echo "" + echo "Expands to:" + echo "$cmd" + echo "" + echo "Type: $(type $name 2>/dev/null || echo "alias")"' \ + --preview-window=right:50%:wrap \ + --header="Enter: insert alias | Ctrl-E: edit definition") + + [[ -z "$selected" ]] && return + + local alias_name="${selected%%=*}" + print -z "$alias_name " +} + +# ============================================================================ +# Function Browser +# ============================================================================ + +funcf() { + _fzf_check || return 1 + + local selected=$(print -l ${(ok)functions} | grep -v "^_" | sort | \ + fzf $(_fzf_common_opts) \ + --prompt="Function > " \ + --preview='whence -f {}' \ + --preview-window=right:60%:wrap \ + --header="Shell functions | Enter: insert | Ctrl-V: view source") + + [[ -z "$selected" ]] && return + + print -z "$selected " +} + +# ============================================================================ +# Enhanced History Search +# ============================================================================ + +histf() { + _fzf_check || return 1 + + local selected=$(fc -ln 1 | tac | awk '!seen[$0]++' | \ + fzf $(_fzf_common_opts) \ + --prompt="History > " \ + --no-sort \ + --header="Command history (newest first) | Enter: execute | Ctrl-E: edit") + + [[ -z "$selected" ]] && return + + print -z "$selected" +} + +# ============================================================================ +# File Finder (enhanced) +# ============================================================================ + +ff() { + _fzf_check || return 1 + + local search_dir="${1:-.}" + local query="${2:-}" + + local cmd="find $search_dir -type f 2>/dev/null" + + # Use fd if available (faster) + if command -v fd &>/dev/null; then + cmd="fd --type f . $search_dir" + fi + + local selected=$(eval "$cmd" | \ + fzf $(_fzf_common_opts) \ + --query="$query" \ + --prompt="File > " \ + --preview=' + file={} + if file "$file" | grep -q "text"; then + bat --style=numbers --color=always "$file" 2>/dev/null || cat "$file" + else + file "$file" + echo "" + ls -lh "$file" + fi' \ + --preview-window=right:60% \ + --header="Files | Enter: open | Ctrl-E: edit | Ctrl-Y: copy path") + + [[ -z "$selected" ]] && return + + echo "$selected" +} + +# Open file with appropriate application +ffo() { + local file=$(ff "$@") + [[ -z "$file" ]] && return + + if [[ -f "$file" ]]; then + if file "$file" | grep -q "text"; then + ${EDITOR:-vim} "$file" + else + xdg-open "$file" 2>/dev/null || open "$file" 2>/dev/null + fi + fi +} + +# ============================================================================ +# Directory Finder +# ============================================================================ + +fdir() { + _fzf_check || return 1 + + local search_dir="${1:-.}" + + local cmd="find $search_dir -type d 2>/dev/null" + + if command -v fd &>/dev/null; then + cmd="fd --type d . $search_dir" + fi + + local selected=$(eval "$cmd" | \ + fzf $(_fzf_common_opts) \ + --prompt="Directory > " \ + --preview='ls -la {} | head -30' \ + --preview-window=right:50% \ + --header="Directories | Enter: cd") + + [[ -z "$selected" ]] && return + + cd "$selected" +} + +# ============================================================================ +# Git Helpers +# ============================================================================ + +# Git branch switcher +gbf() { + _fzf_check || return 1 + + if ! git rev-parse --git-dir &>/dev/null; then + df_print_error "Not a git repository" + return 1 + fi + + local selected=$(git branch -a --color=always | grep -v '/HEAD\s' | \ + fzf $(_fzf_common_opts) \ + --ansi \ + --prompt="Branch > " \ + --preview='git log --oneline --graph --color=always $(echo {} | sed "s/.* //" | sed "s#remotes/##") -- | head -20' \ + --header="Git branches | Enter: checkout") + + [[ -z "$selected" ]] && return + + local branch=$(echo "$selected" | sed "s/.* //" | sed "s#remotes/[^/]*/##") + git checkout "$branch" +} + +# Git commit browser +glogf() { + _fzf_check || return 1 + + if ! git rev-parse --git-dir &>/dev/null; then + df_print_error "Not a git repository" + return 1 + fi + + local selected=$(git log --oneline --color=always | \ + fzf $(_fzf_common_opts) \ + --ansi \ + --prompt="Commit > " \ + --preview='git show --color=always $(echo {} | cut -d" " -f1)' \ + --preview-window=right:60% \ + --header="Git commits | Enter: show | Ctrl-D: diff") + + [[ -z "$selected" ]] && return + + local commit=$(echo "$selected" | cut -d" " -f1) + git show "$commit" +} + +# ============================================================================ +# Help +# ============================================================================ + +fzf-help() { + df_print_func_name "FZF Utilities" + cat << 'EOF' + + Environment: + envf Browse environment variables + env-set [VAR] Set/edit environment variable + + Path: + pathf Explore PATH directories + path-add [DIR] Add directory to PATH + + Process: + procf Browse and manage processes + killf Fuzzy kill processes + + Shell: + aliasf Browse aliases + funcf Browse functions + histf Search command history + + Files: + ff [DIR] Find files + ffo [DIR] Find and open file + fdir [DIR] Find and cd to directory + + Git: + gbf Branch switcher + glogf Commit browser + +EOF +} + +# ============================================================================ +# Aliases +# ============================================================================ + +alias envbrowse='envf' +alias pathbrowse='pathf' +alias proc='procf' diff --git a/zsh/functions/notifications.zsh b/zsh/functions/notifications.zsh new file mode 100644 index 0000000..e9acf97 --- /dev/null +++ b/zsh/functions/notifications.zsh @@ -0,0 +1,293 @@ +# ============================================================================ +# Long-Running Command Notifications +# ============================================================================ +# Sends notifications when long-running commands complete. +# Integrates with the existing timer in the adlee theme. +# +# Features: +# - Desktop notifications (notify-send/libnotify) +# - Terminal bell fallback +# - Sound notification (optional) +# - Configurable thresholds +# - Smart filtering (no notifications for editors, etc.) +# ============================================================================ + +# Prevent double-sourcing +[[ -n "$_DF_NOTIFY_LOADED" ]] && return 0 +typeset -g _DF_NOTIFY_LOADED=1 + +# ============================================================================ +# Configuration +# ============================================================================ + +# Minimum duration (seconds) before notification is sent +typeset -g DF_NOTIFY_THRESHOLD="${DF_NOTIFY_THRESHOLD:-60}" + +# Enable/disable notifications +typeset -g DF_NOTIFY_ENABLED="${DF_NOTIFY_ENABLED:-true}" + +# Notification methods (space-separated): desktop bell sound +typeset -g DF_NOTIFY_METHODS="${DF_NOTIFY_METHODS:-desktop bell}" + +# Sound file for audio notification (optional) +typeset -g DF_NOTIFY_SOUND="${DF_NOTIFY_SOUND:-/usr/share/sounds/freedesktop/stereo/complete.oga}" + +# Commands to ignore (editors, pagers, interactive tools) +typeset -g DF_NOTIFY_IGNORE_CMDS="${DF_NOTIFY_IGNORE_CMDS:-vim nvim nano vi less more man htop top btop watch ssh tmux}" + +# Only notify on failure (exit code != 0) +typeset -g DF_NOTIFY_ONLY_FAILURES="${DF_NOTIFY_ONLY_FAILURES:-false}" + +# ============================================================================ +# Internal State +# ============================================================================ + +typeset -g _df_notify_cmd="" +typeset -g _df_notify_start=0 + +# ============================================================================ +# Notification Functions +# ============================================================================ + +# Check if command should be ignored +_df_notify_should_ignore() { + local cmd="$1" + local first_word="${cmd%% *}" + + # Check against ignore list + for ignore in ${(s: :)DF_NOTIFY_IGNORE_CMDS}; do + [[ "$first_word" == "$ignore" ]] && return 0 + done + + # Ignore backgrounded commands + [[ "$cmd" == *'&'* ]] && return 0 + + # Ignore commands run with nohup + [[ "$cmd" == nohup* ]] && return 0 + + return 1 +} + +# Send desktop notification +_df_notify_desktop() { + local title="$1" + local body="$2" + local urgency="${3:-normal}" + local icon="${4:-terminal}" + + if command -v notify-send &>/dev/null; then + notify-send --urgency="$urgency" --icon="$icon" --app-name="Terminal" "$title" "$body" 2>/dev/null + return 0 + fi + + # macOS fallback + if command -v osascript &>/dev/null; then + osascript -e "display notification \"$body\" with title \"$title\"" 2>/dev/null + return 0 + fi + + return 1 +} + +# Send terminal bell +_df_notify_bell() { + printf '\a' +} + +# Play sound notification +_df_notify_sound() { + local sound_file="$1" + + if [[ -f "$sound_file" ]]; then + if command -v paplay &>/dev/null; then + paplay "$sound_file" &>/dev/null & + elif command -v aplay &>/dev/null; then + aplay -q "$sound_file" &>/dev/null & + elif command -v afplay &>/dev/null; then + afplay "$sound_file" &>/dev/null & + fi + fi +} + +# Format duration for display +_df_notify_format_duration() { + local secs=$1 + + if (( secs >= 3600 )); then + printf "%dh %dm %ds" $((secs/3600)) $((secs%3600/60)) $((secs%60)) + elif (( secs >= 60 )); then + printf "%dm %ds" $((secs/60)) $((secs%60)) + else + printf "%ds" $secs + fi +} + +# Main notification function +_df_notify_send() { + local cmd="$1" + local exit_code="$2" + local duration="$3" + + # Skip if disabled + [[ "$DF_NOTIFY_ENABLED" != "true" ]] && return + + # Skip if below threshold + (( duration < DF_NOTIFY_THRESHOLD )) && return + + # Skip ignored commands + _df_notify_should_ignore "$cmd" && return + + # Skip if only failures and this succeeded + [[ "$DF_NOTIFY_ONLY_FAILURES" == "true" && $exit_code -eq 0 ]] && return + + # Build notification content + local title icon urgency + local duration_str=$(_df_notify_format_duration "$duration") + local cmd_short="${cmd:0:50}" + [[ ${#cmd} -gt 50 ]] && cmd_short="${cmd_short}..." + + if (( exit_code == 0 )); then + title="✓ Command Complete" + icon="dialog-information" + urgency="normal" + else + title="✗ Command Failed (exit $exit_code)" + icon="dialog-error" + urgency="critical" + fi + + local body="$cmd_short\nDuration: $duration_str" + + # Send notifications based on configured methods + for method in ${(s: :)DF_NOTIFY_METHODS}; do + case "$method" in + desktop) + _df_notify_desktop "$title" "$body" "$urgency" "$icon" + ;; + bell) + _df_notify_bell + ;; + sound) + [[ -n "$DF_NOTIFY_SOUND" ]] && _df_notify_sound "$DF_NOTIFY_SOUND" + ;; + esac + done +} + +# ============================================================================ +# Hook Functions +# ============================================================================ + +# Called before command execution +_df_notify_preexec() { + _df_notify_cmd="$1" + _df_notify_start=$SECONDS +} + +# Called after command completion +_df_notify_precmd() { + local exit_code=$? + + # Skip if no command was tracked + [[ -z "$_df_notify_cmd" ]] && return + [[ $_df_notify_start -eq 0 ]] && return + + local duration=$((SECONDS - _df_notify_start)) + + # Send notification + _df_notify_send "$_df_notify_cmd" "$exit_code" "$duration" + + # Reset state + _df_notify_cmd="" + _df_notify_start=0 +} + +# ============================================================================ +# User Commands +# ============================================================================ + +# Toggle notifications +df_notify_toggle() { + if [[ "$DF_NOTIFY_ENABLED" == "true" ]]; then + DF_NOTIFY_ENABLED="false" + echo "Notifications: OFF" + else + DF_NOTIFY_ENABLED="true" + echo "Notifications: ON" + fi +} + +# Set notification threshold +df_notify_threshold() { + if [[ -z "$1" ]]; then + echo "Current threshold: ${DF_NOTIFY_THRESHOLD}s" + echo "Usage: df_notify_threshold " + else + DF_NOTIFY_THRESHOLD="$1" + echo "Threshold set to: ${DF_NOTIFY_THRESHOLD}s" + fi +} + +# Test notification +df_notify_test() { + echo "Sending test notification..." + _df_notify_desktop "Test Notification" "This is a test notification from dotfiles" "normal" "terminal" + _df_notify_bell + echo "Done. Did you see/hear it?" +} + +# Show notification status +df_notify_status() { + source "${DOTFILES_DIR:-$HOME/.dotfiles}/zsh/lib/bootstrap.zsh" 2>/dev/null + + df_print_func_name "Notification Status" + echo "" + df_print_section "Configuration" + df_print_indent "Enabled: $DF_NOTIFY_ENABLED" + df_print_indent "Threshold: ${DF_NOTIFY_THRESHOLD}s" + df_print_indent "Methods: $DF_NOTIFY_METHODS" + df_print_indent "Only fail: $DF_NOTIFY_ONLY_FAILURES" + + echo "" + df_print_section "Capabilities" + + if command -v notify-send &>/dev/null; then + df_print_indent "Desktop: ✓ (notify-send)" + elif command -v osascript &>/dev/null; then + df_print_indent "Desktop: ✓ (osascript/macOS)" + else + df_print_indent "Desktop: ✗ (install libnotify)" + fi + + df_print_indent "Bell: ✓ (always available)" + + if [[ -n "$DF_NOTIFY_SOUND" && -f "$DF_NOTIFY_SOUND" ]]; then + df_print_indent "Sound: ✓ ($DF_NOTIFY_SOUND)" + else + df_print_indent "Sound: ✗ (no sound file configured)" + fi + + echo "" + df_print_section "Ignored Commands" + df_print_indent "$DF_NOTIFY_IGNORE_CMDS" +} + +# ============================================================================ +# Aliases +# ============================================================================ + +alias notify-toggle='df_notify_toggle' +alias notify-test='df_notify_test' +alias notify-status='df_notify_status' + +# ============================================================================ +# Initialize Hooks +# ============================================================================ + +# Only set up hooks if not already done (avoid duplicates) +if [[ -z "$_DF_NOTIFY_HOOKS_SET" ]]; then + autoload -Uz add-zsh-hook + add-zsh-hook preexec _df_notify_preexec + add-zsh-hook precmd _df_notify_precmd + typeset -g _DF_NOTIFY_HOOKS_SET=1 +fi diff --git a/zsh/functions/project-env.zsh b/zsh/functions/project-env.zsh new file mode 100644 index 0000000..85099e7 --- /dev/null +++ b/zsh/functions/project-env.zsh @@ -0,0 +1,423 @@ +# ============================================================================ +# Project-Local Environment Manager +# ============================================================================ +# Automatically activates project-specific settings when entering directories. +# Similar to direnv but integrated with dotfiles. +# +# Features: +# - Auto-load .dotfiles-local or .envrc files +# - Virtual environment auto-activation +# - Node version switching (via nvm) +# - Custom environment variables per project +# - Security: prompts before loading untrusted files +# ============================================================================ + +# Prevent double-sourcing +[[ -n "$_DF_PROJECT_ENV_LOADED" ]] && return 0 +typeset -g _DF_PROJECT_ENV_LOADED=1 + +# ============================================================================ +# Configuration +# ============================================================================ + +# Enable/disable auto-loading +typeset -g DF_PROJECT_ENV_ENABLED="${DF_PROJECT_ENV_ENABLED:-true}" + +# Files to look for (in order of priority) +typeset -g DF_PROJECT_ENV_FILES="${DF_PROJECT_ENV_FILES:-.dotfiles-local .envrc .env.local}" + +# Trusted directories (auto-allow without prompt) +typeset -g DF_PROJECT_ENV_TRUSTED_DIRS="${DF_PROJECT_ENV_TRUSTED_DIRS:-$HOME/projects $HOME/work $HOME/.dotfiles}" + +# Store allowed files +typeset -g DF_PROJECT_ENV_ALLOWED_FILE="${XDG_DATA_HOME:-$HOME/.local/share}/dotfiles/allowed-envs" + +# Auto-activate Python virtualenvs +typeset -g DF_PROJECT_AUTO_VENV="${DF_PROJECT_AUTO_VENV:-true}" + +# Auto-switch Node versions via .nvmrc +typeset -g DF_PROJECT_AUTO_NVM="${DF_PROJECT_AUTO_NVM:-true}" + +# ============================================================================ +# Internal State +# ============================================================================ + +typeset -g _df_project_current_env="" +typeset -g _df_project_original_path="$PATH" +typeset -gA _df_project_original_vars=() + +# ============================================================================ +# Helper Functions +# ============================================================================ + +# Check if a path is in trusted directories +_df_project_is_trusted() { + local dir="$1" + + for trusted in ${(s: :)DF_PROJECT_ENV_TRUSTED_DIRS}; do + [[ "$dir" == "$trusted"* ]] && return 0 + done + + return 1 +} + +# Check if file is explicitly allowed +_df_project_is_allowed() { + local file="$1" + local file_hash=$(sha256sum "$file" 2>/dev/null | cut -d' ' -f1) + + [[ ! -f "$DF_PROJECT_ENV_ALLOWED_FILE" ]] && return 1 + + grep -q "^${file}:${file_hash}$" "$DF_PROJECT_ENV_ALLOWED_FILE" 2>/dev/null +} + +# Add file to allowed list +_df_project_allow_file() { + local file="$1" + local file_hash=$(sha256sum "$file" 2>/dev/null | cut -d' ' -f1) + + mkdir -p "$(dirname "$DF_PROJECT_ENV_ALLOWED_FILE")" + + # Remove old entry if exists + if [[ -f "$DF_PROJECT_ENV_ALLOWED_FILE" ]]; then + grep -v "^${file}:" "$DF_PROJECT_ENV_ALLOWED_FILE" > "${DF_PROJECT_ENV_ALLOWED_FILE}.tmp" 2>/dev/null || true + mv "${DF_PROJECT_ENV_ALLOWED_FILE}.tmp" "$DF_PROJECT_ENV_ALLOWED_FILE" + fi + + echo "${file}:${file_hash}" >> "$DF_PROJECT_ENV_ALLOWED_FILE" +} + +# Save current environment variable +_df_project_save_var() { + local var="$1" + if [[ -z "${_df_project_original_vars[$var]+x}" ]]; then + _df_project_original_vars[$var]="${(P)var}" + fi +} + +# Restore saved environment variable +_df_project_restore_var() { + local var="$1" + if [[ -n "${_df_project_original_vars[$var]+x}" ]]; then + export "$var"="${_df_project_original_vars[$var]}" + unset "_df_project_original_vars[$var]" + fi +} + +# ============================================================================ +# Environment Loading +# ============================================================================ + +# Load a project environment file +_df_project_load_env() { + local env_file="$1" + + [[ ! -f "$env_file" ]] && return 1 + + # Security check + if ! _df_project_is_trusted "$(dirname "$env_file")" && ! _df_project_is_allowed "$env_file"; then + echo "" + echo -e "${DF_YELLOW}⚠${DF_NC} Found project env: $env_file" + echo -e "${DF_DIM}$(head -5 "$env_file")${DF_NC}" + echo "" + + if read -q "?Allow loading this file? [y/N] "; then + echo "" + _df_project_allow_file "$env_file" + else + echo "" + echo "Skipped. To allow later: project-env allow $env_file" + return 1 + fi + fi + + # Save current PATH + _df_project_save_var "PATH" + + # Source the file + _df_project_current_env="$env_file" + source "$env_file" + + # Visual indicator + local project_name=$(basename "$(dirname "$env_file")") + echo -e "${DF_GREEN}●${DF_NC} Project: ${DF_CYAN}${project_name}${DF_NC}" +} + +# Unload current project environment +_df_project_unload_env() { + [[ -z "$_df_project_current_env" ]] && return + + # Restore PATH + _df_project_restore_var "PATH" + + # Deactivate virtualenv if active + [[ -n "$VIRTUAL_ENV" ]] && deactivate 2>/dev/null + + local project_name=$(basename "$(dirname "$_df_project_current_env")") + echo -e "${DF_DIM}○ Left: ${project_name}${DF_NC}" + + _df_project_current_env="" +} + +# ============================================================================ +# Auto-Detection +# ============================================================================ + +# Auto-activate Python virtualenv +_df_project_auto_venv() { + [[ "$DF_PROJECT_AUTO_VENV" != "true" ]] && return + + local venv_dirs=("venv" ".venv" "env" ".env") + + for dir in "${venv_dirs[@]}"; do + if [[ -f "$dir/bin/activate" ]]; then + source "$dir/bin/activate" + echo -e "${DF_GREEN}●${DF_NC} Virtualenv: ${DF_CYAN}${dir}${DF_NC}" + return + fi + done +} + +# Auto-switch Node version via .nvmrc +_df_project_auto_nvm() { + [[ "$DF_PROJECT_AUTO_NVM" != "true" ]] && return + [[ ! -f ".nvmrc" ]] && return + + # Check if nvm is available + if command -v nvm &>/dev/null || [[ -s "$NVM_DIR/nvm.sh" ]]; then + # Load nvm if not loaded + [[ -z "$(command -v nvm)" && -s "$NVM_DIR/nvm.sh" ]] && source "$NVM_DIR/nvm.sh" + + local nvmrc_version=$(cat .nvmrc) + local current_version=$(node --version 2>/dev/null || echo "none") + + if [[ "$current_version" != "$nvmrc_version"* ]]; then + echo -e "${DF_GREEN}●${DF_NC} Node: ${DF_CYAN}${nvmrc_version}${DF_NC}" + nvm use 2>/dev/null + fi + fi +} + +# ============================================================================ +# Directory Change Hook +# ============================================================================ + +_df_project_chpwd_hook() { + [[ "$DF_PROJECT_ENV_ENABLED" != "true" ]] && return + + local current_dir="$PWD" + + # Check if we left a project directory + if [[ -n "$_df_project_current_env" ]]; then + local env_dir=$(dirname "$_df_project_current_env") + if [[ "$current_dir" != "$env_dir"* ]]; then + _df_project_unload_env + fi + fi + + # Look for project env files + for env_file in ${(s: :)DF_PROJECT_ENV_FILES}; do + if [[ -f "$current_dir/$env_file" ]]; then + _df_project_load_env "$current_dir/$env_file" + break + fi + done + + # Auto-activate virtualenv + _df_project_auto_venv + + # Auto-switch Node version + _df_project_auto_nvm +} + +# ============================================================================ +# User Commands +# ============================================================================ + +# Main project-env command +project-env() { + local cmd="${1:-status}" + shift 2>/dev/null || true + + case "$cmd" in + status|s) + source "${DOTFILES_DIR:-$HOME/.dotfiles}/zsh/lib/bootstrap.zsh" 2>/dev/null + + df_print_func_name "Project Environment Status" + echo "" + df_print_section "Configuration" + df_print_indent "Enabled: $DF_PROJECT_ENV_ENABLED" + df_print_indent "Auto venv: $DF_PROJECT_AUTO_VENV" + df_print_indent "Auto nvm: $DF_PROJECT_AUTO_NVM" + df_print_indent "Env files: $DF_PROJECT_ENV_FILES" + + echo "" + df_print_section "Current State" + if [[ -n "$_df_project_current_env" ]]; then + df_print_indent "Active env: $_df_project_current_env" + else + df_print_indent "Active env: (none)" + fi + + if [[ -n "$VIRTUAL_ENV" ]]; then + df_print_indent "Virtualenv: $VIRTUAL_ENV" + fi + + echo "" + df_print_section "Trusted Directories" + for dir in ${(s: :)DF_PROJECT_ENV_TRUSTED_DIRS}; do + df_print_indent "● $dir" + done + ;; + + allow|a) + local file="${1:-$PWD/.dotfiles-local}" + if [[ -f "$file" ]]; then + _df_project_allow_file "$file" + echo "Allowed: $file" + else + echo "File not found: $file" + fi + ;; + + deny|d) + local file="${1:-$PWD/.dotfiles-local}" + if [[ -f "$DF_PROJECT_ENV_ALLOWED_FILE" ]]; then + grep -v "^${file}:" "$DF_PROJECT_ENV_ALLOWED_FILE" > "${DF_PROJECT_ENV_ALLOWED_FILE}.tmp" 2>/dev/null || true + mv "${DF_PROJECT_ENV_ALLOWED_FILE}.tmp" "$DF_PROJECT_ENV_ALLOWED_FILE" + echo "Denied: $file" + fi + ;; + + list|l) + echo "Allowed environment files:" + if [[ -f "$DF_PROJECT_ENV_ALLOWED_FILE" ]]; then + cat "$DF_PROJECT_ENV_ALLOWED_FILE" | cut -d: -f1 | while read -r file; do + if [[ -f "$file" ]]; then + echo -e " ${DF_GREEN}✓${DF_NC} $file" + else + echo -e " ${DF_RED}✗${DF_NC} $file (missing)" + fi + done + else + echo " (none)" + fi + ;; + + create|c) + local file="${1:-.dotfiles-local}" + if [[ -f "$file" ]]; then + echo "File already exists: $file" + return 1 + fi + + cat > "$file" << 'EOF' +# ============================================================================ +# Project-Local Environment +# ============================================================================ +# This file is automatically loaded when entering this directory. +# Add project-specific settings below. +# ============================================================================ + +# --- Environment Variables --- +# export PROJECT_NAME="myproject" +# export DATABASE_URL="postgresql://localhost/mydb" + +# --- Path Additions --- +# export PATH="$PWD/bin:$PATH" + +# --- Virtual Environment --- +# [[ -f venv/bin/activate ]] && source venv/bin/activate + +# --- Custom Aliases --- +# alias build='./scripts/build.sh' +# alias test='pytest' + +# --- Startup Message --- +# echo "Welcome to $(basename $PWD)!" +EOF + + echo "Created: $file" + echo "Edit with: \${EDITOR:-vim} $file" + ;; + + edit|e) + local file="" + for env_file in ${(s: :)DF_PROJECT_ENV_FILES}; do + [[ -f "$env_file" ]] && { file="$env_file"; break; } + done + + if [[ -n "$file" ]]; then + ${EDITOR:-vim} "$file" + else + echo "No project env file found. Create one: project-env create" + fi + ;; + + reload|r) + _df_project_chpwd_hook + ;; + + off) + DF_PROJECT_ENV_ENABLED="false" + _df_project_unload_env + echo "Project environments disabled" + ;; + + on) + DF_PROJECT_ENV_ENABLED="true" + _df_project_chpwd_hook + echo "Project environments enabled" + ;; + + help|--help|-h) + cat << 'EOF' +Project Environment Manager + +Usage: project-env [args] + +Commands: + status, s Show current status + allow Trust a project env file + deny Remove trust for a file + list, l List allowed files + create [file] Create a new project env file + edit, e Edit current project's env file + reload, r Reload current directory's env + on/off Enable/disable auto-loading + +Files checked (in order): .dotfiles-local, .envrc, .env.local + +Examples: + project-env create # Create .dotfiles-local + project-env allow # Trust current dir's env file + project-env off # Disable auto-loading +EOF + ;; + + *) + echo "Unknown command: $cmd" + echo "Use 'project-env help' for usage" + ;; + esac +} + +# ============================================================================ +# Aliases +# ============================================================================ + +alias penv='project-env' +alias penv-create='project-env create' +alias penv-edit='project-env edit' + +# ============================================================================ +# Initialize Hook +# ============================================================================ + +if [[ "$DF_PROJECT_ENV_ENABLED" == "true" ]]; then + autoload -Uz add-zsh-hook + add-zsh-hook chpwd _df_project_chpwd_hook + + # Run on initial shell load + _df_project_chpwd_hook +fi diff --git a/zsh/lib/machines.zsh b/zsh/lib/machines.zsh new file mode 100644 index 0000000..4f748a3 --- /dev/null +++ b/zsh/lib/machines.zsh @@ -0,0 +1,230 @@ +# ============================================================================ +# Machine-Specific Configuration Loader +# ============================================================================ +# Automatically loads configuration based on hostname, allowing different +# settings per machine while keeping a single dotfiles repository. +# +# Configuration hierarchy (later files override earlier): +# 1. dotfiles.conf (base config) +# 2. machines/default.zsh (shared overrides) +# 3. machines/.zsh (machine-specific) +# 4. ~/.zshrc.local (local user overrides) +# +# Usage: +# Create ~/.dotfiles/machines/.zsh for machine-specific settings +# Use `df_machine_info` to see current machine detection +# ============================================================================ + +# Prevent double-sourcing +[[ -n "$_DF_MACHINES_LOADED" ]] && return 0 +typeset -g _DF_MACHINES_LOADED=1 + +# ============================================================================ +# Machine Detection +# ============================================================================ + +typeset -g DF_HOSTNAME="${HOST:-${HOSTNAME:-$(hostname -s 2>/dev/null)}}" +typeset -g DF_HOSTNAME_FULL="$(hostname -f 2>/dev/null || echo "$DF_HOSTNAME")" +typeset -g DF_MACHINE_TYPE="unknown" +typeset -g DF_MACHINE_CONFIG="" + +# Detect machine type based on hostname patterns or hardware +_df_detect_machine_type() { + local hostname="$DF_HOSTNAME" + + # Check for common naming patterns + case "$hostname" in + *laptop*|*book*|*portable*|*mobile*) + DF_MACHINE_TYPE="laptop" + ;; + *server*|*srv*|*node*|*host*) + DF_MACHINE_TYPE="server" + ;; + *desktop*|*workstation*|*ws*|*pc*) + DF_MACHINE_TYPE="desktop" + ;; + *vm*|*virtual*|*container*) + DF_MACHINE_TYPE="virtual" + ;; + *) + # Try to detect from hardware + if [[ -d /sys/class/power_supply/BAT0 ]]; then + DF_MACHINE_TYPE="laptop" + elif [[ -f /proc/cpuinfo ]] && grep -qi "hypervisor\|vmware\|virtualbox\|kvm\|xen" /proc/cpuinfo 2>/dev/null; then + DF_MACHINE_TYPE="virtual" + elif systemd-detect-virt &>/dev/null && [[ "$(systemd-detect-virt)" != "none" ]]; then + DF_MACHINE_TYPE="virtual" + else + DF_MACHINE_TYPE="desktop" + fi + ;; + esac +} + +# ============================================================================ +# Configuration Loading +# ============================================================================ + +_df_load_machine_config() { + local machines_dir="${DOTFILES_DIR:-$HOME/.dotfiles}/machines" + local loaded=() + + # Create machines directory if it doesn't exist + [[ ! -d "$machines_dir" ]] && mkdir -p "$machines_dir" + + # 1. Load default machine config (shared across all machines) + if [[ -f "$machines_dir/default.zsh" ]]; then + source "$machines_dir/default.zsh" + loaded+=("default") + fi + + # 2. Load machine-type specific config + if [[ -f "$machines_dir/type-${DF_MACHINE_TYPE}.zsh" ]]; then + source "$machines_dir/type-${DF_MACHINE_TYPE}.zsh" + loaded+=("type-${DF_MACHINE_TYPE}") + fi + + # 3. Load hostname-specific config (highest priority) + if [[ -f "$machines_dir/${DF_HOSTNAME}.zsh" ]]; then + source "$machines_dir/${DF_HOSTNAME}.zsh" + loaded+=("$DF_HOSTNAME") + DF_MACHINE_CONFIG="$machines_dir/${DF_HOSTNAME}.zsh" + fi + + # Store what was loaded for debugging + typeset -g DF_MACHINE_CONFIGS_LOADED="${(j:, :)loaded}" +} + +# ============================================================================ +# Machine Information Commands +# ============================================================================ + +# Display machine detection info +df_machine_info() { + source "${DOTFILES_DIR:-$HOME/.dotfiles}/zsh/lib/bootstrap.zsh" 2>/dev/null + + df_print_func_name "Machine Configuration" + echo "" + df_print_section "Detection" + df_print_indent "Hostname: $DF_HOSTNAME" + df_print_indent "Full hostname: $DF_HOSTNAME_FULL" + df_print_indent "Machine type: $DF_MACHINE_TYPE" + echo "" + df_print_section "Loaded Configs" + if [[ -n "$DF_MACHINE_CONFIGS_LOADED" ]]; then + df_print_indent "$DF_MACHINE_CONFIGS_LOADED" + else + df_print_indent "(none)" + fi + echo "" + df_print_section "Config File" + if [[ -n "$DF_MACHINE_CONFIG" ]]; then + df_print_indent "$DF_MACHINE_CONFIG" + else + df_print_indent "No machine-specific config found" + df_print_info "Create: ${DOTFILES_DIR:-$HOME/.dotfiles}/machines/${DF_HOSTNAME}.zsh" + fi +} + +# Create a new machine config from template +df_machine_create() { + local hostname="${1:-$DF_HOSTNAME}" + local machines_dir="${DOTFILES_DIR:-$HOME/.dotfiles}/machines" + local config_file="$machines_dir/${hostname}.zsh" + + source "${DOTFILES_DIR:-$HOME/.dotfiles}/zsh/lib/bootstrap.zsh" 2>/dev/null + + if [[ -f "$config_file" ]]; then + df_print_warning "Config already exists: $config_file" + df_confirm "Edit existing config?" && ${EDITOR:-vim} "$config_file" + return + fi + + df_print_step "Creating machine config: $hostname" + + cat > "$config_file" << EOF +# ============================================================================ +# Machine Configuration: ${hostname} +# ============================================================================ +# This file is automatically loaded when the hostname matches. +# Created: $(date '+%Y-%m-%d %H:%M') +# +# Available variables to override: +# DF_WIDTH, MOTD_STYLE, ENABLE_MOTD, ZSH_THEME_NAME +# Any variable from dotfiles.conf +# ============================================================================ + +# --- Display Settings --- +# DF_WIDTH="80" # Wider terminal? +# MOTD_STYLE="mini" # mini, compact, full, none + +# --- Machine-specific paths --- +# export PATH="\$HOME/custom-tools:\$PATH" + +# --- Machine-specific aliases --- +# alias proj='cd ~/projects/work' + +# --- Machine-specific environment --- +# export JAVA_HOME="/usr/lib/jvm/java-17" + +# --- Conditional features --- +# Example: Disable heavy features on slow machines +# ENABLE_SMART_SUGGESTIONS="false" + +# --- SSH agent (if needed on this machine) --- +# if [[ -z "\$SSH_AUTH_SOCK" ]]; then +# eval "\$(ssh-agent -s)" &>/dev/null +# ssh-add ~/.ssh/id_ed25519 2>/dev/null +# fi + +# --- Custom startup commands --- +# echo "Welcome to ${hostname}!" +EOF + + df_print_success "Created: $config_file" + df_print_info "Edit with: ${EDITOR:-vim} $config_file" + + df_confirm "Edit now?" && ${EDITOR:-vim} "$config_file" +} + +# List all machine configs +df_machine_list() { + local machines_dir="${DOTFILES_DIR:-$HOME/.dotfiles}/machines" + + source "${DOTFILES_DIR:-$HOME/.dotfiles}/zsh/lib/bootstrap.zsh" 2>/dev/null + + df_print_func_name "Machine Configurations" + echo "" + + if [[ ! -d "$machines_dir" ]] || [[ -z "$(ls -A "$machines_dir" 2>/dev/null)" ]]; then + df_print_info "No machine configs found" + df_print_info "Create one: df_machine_create " + return + fi + + for config in "$machines_dir"/*.zsh(N); do + [[ -f "$config" ]] || continue + local name=$(basename "$config" .zsh) + local marker="" + [[ "$name" == "$DF_HOSTNAME" ]] && marker=" ${DF_GREEN}(current)${DF_NC}" + [[ "$name" == "default" ]] && marker=" ${DF_CYAN}(shared)${DF_NC}" + [[ "$name" == type-* ]] && marker=" ${DF_YELLOW}(type)${DF_NC}" + df_print_indent "● ${name}${marker}" + done +} + +# ============================================================================ +# Aliases +# ============================================================================ + +alias machines='df_machine_list' +alias machine-info='df_machine_info' +alias machine-create='df_machine_create' +alias machine-edit='${EDITOR:-vim} "${DOTFILES_DIR:-$HOME/.dotfiles}/machines/${DF_HOSTNAME}.zsh"' + +# ============================================================================ +# Initialize +# ============================================================================ + +_df_detect_machine_type +_df_load_machine_config diff --git a/zsh/lib/plugins.zsh b/zsh/lib/plugins.zsh new file mode 100644 index 0000000..d1af08b --- /dev/null +++ b/zsh/lib/plugins.zsh @@ -0,0 +1,301 @@ +# ============================================================================ +# Dotfiles Plugin Manager +# ============================================================================ +# A thin wrapper for managing zsh plugins without heavy frameworks. +# +# Features: +# - Simple git-based plugin installation +# - Automatic updates +# - Lazy loading support +# - Oh-My-Zsh plugin compatibility +# ============================================================================ + +# Prevent double-sourcing +[[ -n "$_DF_PLUGINS_LOADED" ]] && return 0 +typeset -g _DF_PLUGINS_LOADED=1 + +# ============================================================================ +# Configuration +# ============================================================================ + +typeset -g DF_PLUGIN_DIR="${DF_PLUGIN_DIR:-$HOME/.dotfiles/zsh/plugins}" +typeset -g DF_PLUGIN_REPOS_FILE="${DF_PLUGIN_DIR}/.repos" + +# Track loaded plugins +typeset -ga DF_LOADED_PLUGINS=() + +# ============================================================================ +# Core Functions +# ============================================================================ + +# Install a plugin from GitHub +# Usage: df_plugin "zsh-users/zsh-autosuggestions" [branch] +df_plugin() { + local repo="$1" + local branch="${2:-master}" + local name="${repo##*/}" + local dir="$DF_PLUGIN_DIR/$name" + + # Ensure plugin directory exists + [[ ! -d "$DF_PLUGIN_DIR" ]] && mkdir -p "$DF_PLUGIN_DIR" + + # Clone if not exists + if [[ ! -d "$dir" ]]; then + echo "Installing plugin: $name..." + git clone --depth 1 --branch "$branch" "https://github.com/$repo.git" "$dir" 2>/dev/null + if [[ $? -eq 0 ]]; then + echo "✓ Installed: $name" + # Track the repo + echo "$repo|$branch" >> "$DF_PLUGIN_REPOS_FILE" + else + echo "✗ Failed to install: $name" + return 1 + fi + fi + + # Source the plugin + df_plugin_load "$name" +} + +# Load a plugin by name +df_plugin_load() { + local name="$1" + local dir="$DF_PLUGIN_DIR/$name" + + # Check if already loaded + [[ " ${DF_LOADED_PLUGINS[*]} " =~ " $name " ]] && return 0 + + if [[ -d "$dir" ]]; then + # Try common plugin file names + local plugin_files=( + "$dir/$name.plugin.zsh" + "$dir/$name.zsh" + "$dir/init.zsh" + "$dir/$name.sh" + ) + + for file in "${plugin_files[@]}"; do + if [[ -f "$file" ]]; then + source "$file" + DF_LOADED_PLUGINS+=("$name") + return 0 + fi + done + + echo "Warning: Could not find plugin entry point for $name" + return 1 + else + echo "Plugin not found: $name" + return 1 + fi +} + +# Lazy load a plugin (load on first use of command) +# Usage: df_plugin_lazy "plugin-name" "command1" "command2" +df_plugin_lazy() { + local plugin="$1" + shift + local commands=("$@") + + for cmd in "${commands[@]}"; do + eval " + $cmd() { + unfunction $cmd 2>/dev/null + df_plugin_load '$plugin' + $cmd \"\$@\" + } + " + done +} + +# Update all plugins +df_plugin_update() { + source "${DOTFILES_DIR:-$HOME/.dotfiles}/zsh/lib/bootstrap.zsh" 2>/dev/null + + df_print_func_name "Plugin Update" + echo "" + + for dir in "$DF_PLUGIN_DIR"/*/; do + [[ -d "$dir/.git" ]] || continue + + local name=$(basename "$dir") + df_print_step "Updating: $name" + + ( + cd "$dir" + git pull --quiet 2>/dev/null && \ + df_print_success "$name updated" || \ + df_print_warning "$name: update failed" + ) + done +} + +# List installed plugins +df_plugin_list() { + source "${DOTFILES_DIR:-$HOME/.dotfiles}/zsh/lib/bootstrap.zsh" 2>/dev/null + + df_print_func_name "Installed Plugins" + echo "" + + if [[ ! -d "$DF_PLUGIN_DIR" ]] || [[ -z "$(ls -A "$DF_PLUGIN_DIR" 2>/dev/null)" ]]; then + df_print_info "No plugins installed" + return + fi + + for dir in "$DF_PLUGIN_DIR"/*/; do + [[ -d "$dir" ]] || continue + + local name=$(basename "$dir") + local loaded="" + [[ " ${DF_LOADED_PLUGINS[*]} " =~ " $name " ]] && loaded=" ${DF_GREEN}(loaded)${DF_NC}" + + # Get repo info if available + local repo_info="" + if [[ -d "$dir/.git" ]]; then + local remote=$(cd "$dir" && git remote get-url origin 2>/dev/null) + repo_info=" ${DF_DIM}${remote##*github.com/}${DF_NC}" + fi + + df_print_indent "● ${name}${loaded}${repo_info}" + done + + echo "" + df_print_section "Loaded Plugins" + if [[ ${#DF_LOADED_PLUGINS[@]} -gt 0 ]]; then + df_print_indent "${DF_LOADED_PLUGINS[*]}" + else + df_print_indent "(none)" + fi +} + +# Remove a plugin +df_plugin_remove() { + local name="$1" + local dir="$DF_PLUGIN_DIR/$name" + + [[ -z "$name" ]] && { echo "Usage: df_plugin_remove "; return 1; } + + if [[ ! -d "$dir" ]]; then + echo "Plugin not found: $name" + return 1 + fi + + source "${DOTFILES_DIR:-$HOME/.dotfiles}/zsh/lib/bootstrap.zsh" 2>/dev/null + + df_confirm "Remove plugin '$name'?" || return 1 + + rm -rf "$dir" + + # Remove from repos file + if [[ -f "$DF_PLUGIN_REPOS_FILE" ]]; then + grep -v "/$name|" "$DF_PLUGIN_REPOS_FILE" > "${DF_PLUGIN_REPOS_FILE}.tmp" 2>/dev/null || true + mv "${DF_PLUGIN_REPOS_FILE}.tmp" "$DF_PLUGIN_REPOS_FILE" + fi + + df_print_success "Removed: $name" + df_print_info "Restart shell to fully unload" +} + +# Install recommended plugins +df_plugin_recommended() { + source "${DOTFILES_DIR:-$HOME/.dotfiles}/zsh/lib/bootstrap.zsh" 2>/dev/null + + df_print_func_name "Recommended Plugins" + echo "" + + local plugins=( + "zsh-users/zsh-autosuggestions:Fish-like autosuggestions" + "zsh-users/zsh-syntax-highlighting:Syntax highlighting" + "zsh-users/zsh-completions:Additional completions" + "romkatv/zsh-defer:Deferred loading for faster startup" + "Aloxaf/fzf-tab:FZF-powered tab completion" + ) + + for plugin_info in "${plugins[@]}"; do + local repo="${plugin_info%%:*}" + local desc="${plugin_info#*:}" + local name="${repo##*/}" + local status="" + + if [[ -d "$DF_PLUGIN_DIR/$name" ]]; then + status="${DF_GREEN}(installed)${DF_NC}" + else + status="${DF_DIM}(not installed)${DF_NC}" + fi + + echo -e " ${DF_CYAN}$name${DF_NC} $status" + echo -e " ${DF_DIM}$desc${DF_NC}" + echo -e " ${DF_DIM}Install: df_plugin $repo${DF_NC}" + echo "" + done +} + +# ============================================================================ +# Command Interface +# ============================================================================ + +# Main plugin command +plugin() { + local cmd="${1:-list}" + shift 2>/dev/null || true + + case "$cmd" in + install|add|i) + [[ -z "$1" ]] && { echo "Usage: plugin install "; return 1; } + df_plugin "$@" + ;; + load|l) + df_plugin_load "$@" + ;; + lazy) + df_plugin_lazy "$@" + ;; + update|up|u) + df_plugin_update + ;; + list|ls) + df_plugin_list + ;; + remove|rm|r) + df_plugin_remove "$@" + ;; + recommended|rec) + df_plugin_recommended + ;; + help|--help|-h) + cat << 'EOF' +Dotfiles Plugin Manager + +Usage: plugin [args] + +Commands: + install Install plugin from GitHub (e.g., zsh-users/zsh-autosuggestions) + load Load an installed plugin + lazy Lazy-load plugin on command use + update Update all plugins + list List installed plugins + remove Remove a plugin + recommended Show recommended plugins + +Examples: + plugin install zsh-users/zsh-autosuggestions + plugin lazy zsh-nvm nvm node npm + plugin update + plugin remove zsh-autosuggestions + +EOF + ;; + *) + echo "Unknown command: $cmd" + echo "Use 'plugin help' for usage" + return 1 + ;; + esac +} + +# ============================================================================ +# Initialize +# ============================================================================ + +# Ensure plugin directory exists +[[ ! -d "$DF_PLUGIN_DIR" ]] && mkdir -p "$DF_PLUGIN_DIR"