Dotfiles update 2025-12-25 15:45
This commit is contained in:
500
zsh/functions/fzf-extras.zsh
Normal file
500
zsh/functions/fzf-extras.zsh
Normal 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'
|
||||
293
zsh/functions/notifications.zsh
Normal file
293
zsh/functions/notifications.zsh
Normal 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
|
||||
423
zsh/functions/project-env.zsh
Normal file
423
zsh/functions/project-env.zsh
Normal 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
|
||||
Reference in New Issue
Block a user