Dotfiles update 2025-12-25 15:45

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

596
README.md
View File

@@ -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-<type>.zsh` (laptop/desktop/server/virtual)
4. `machines/<hostname>.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 <args>` | `sudo systemctl <args>` |
| `scr <service>` | Restart + show status |
| `sce <service>` | Enable + start |
| `scd <service>` | Disable + stop |
| `sclog <service>` | 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 <name> [template]` | - | Create/attach workspace |
| `tw-list` | `twl` | List active workspaces |
| `tw-templates` | `twt` | Show available templates |
| `tw-save <name>` | `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 <name>` | - | Start/attach project |
| `txi-new <n> [tmpl]` | `txin` | Create project |
| `txi-edit <name>` | `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 <name> [path]` | `bm` | Save bookmark |
| `bookmark list` | `bm list` | List bookmarks |
| `jump <name>` | `j` | Go to bookmark |
---
## Secrets Vault
Encrypted storage for API keys using `age` or `gpg`.
| Command | Description |
|---------|-------------|
| `vault init` | Initialize |
| `vault set <key>` | Store secret |
| `vault get <key>` | Retrieve secret |
| `vault list` | List keys |
| `vault shell` | Export to environment |
---
## Password Manager (LastPass)
| Command | Description |
|---------|-------------|
| `pw <query>` | Search and copy password |
| `pw show <item>` | Show entry details |
| `pw list` | List all entries |
| `pw gen [len]` | Generate password |
| `pwf` | Fuzzy search (fzf) |
---
## Python Templates
| Command | Alias | Description |
|---------|-------|-------------|
| `py-new <name>` | `pynew` | Basic project |
| `py-flask <name>` | `pyflask` | Flask web app |
| `py-fastapi <name>` | `pyfast` | FastAPI REST API |
| `py-cli <name>` | `pycli` | CLI with Click |
| `py-data <name>` | `pydata` | Data science |
| `venv` | - | Activate virtualenv |
---
## SSH Manager
| Command | Alias | Description |
|---------|-------|-------------|
| `ssh-save <n> <user@host>` | `sshs` | Save profile |
| `ssh-connect <name>` | `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

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

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

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

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

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

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

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

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

58
machines/default.zsh Normal file
View File

@@ -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-<type>.zsh (laptop, desktop, server, virtual)
# 4. machines/<hostname>.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"
# }

31
machines/type-laptop.zsh Normal file
View File

@@ -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'

36
machines/type-server.zsh Normal file
View File

@@ -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'

241
tests/run-tests.zsh Executable file
View File

@@ -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 "$@"

94
tests/test_config.zsh Executable file
View File

@@ -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

111
tests/test_utils.zsh Executable file
View File

@@ -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"

206
zsh/aliases-extended.zsh Normal file
View File

@@ -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 <command> [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'

View File

@@ -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'

View File

@@ -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 <seconds>"
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

View File

@@ -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 <command> [args]
Commands:
status, s Show current status
allow <file> Trust a project env file
deny <file> 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

230
zsh/lib/machines.zsh Normal file
View File

@@ -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/<hostname>.zsh (machine-specific)
# 4. ~/.zshrc.local (local user overrides)
#
# Usage:
# Create ~/.dotfiles/machines/<hostname>.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 <hostname>"
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

301
zsh/lib/plugins.zsh Normal file
View File

@@ -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 <name>"; 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 <github-user/repo>"; 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 <command> [args]
Commands:
install <repo> Install plugin from GitHub (e.g., zsh-users/zsh-autosuggestions)
load <name> Load an installed plugin
lazy <n> <cmds> Lazy-load plugin on command use
update Update all plugins
list List installed plugins
remove <name> 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"