Files
dotfiles/zsh/functions/fzf-extras.zsh
2025-12-25 16:43:10 -05:00

550 lines
17 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ============================================================================
# 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 || {
# Fallback definitions
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_print_error() { echo -e "${DF_RED}${DF_NC} $1" >&2; }
df_print_success() { echo -e "${DF_GREEN}${DF_NC} $1"; }
df_print_warning() { echo -e "${DF_YELLOW}${DF_NC} $1"; }
df_print_info() { echo -e "${DF_CYAN}${DF_NC} $1"; }
df_print_step() { echo -e "${DF_BLUE}==>${DF_NC} $1"; }
df_print_section() { echo -e "${DF_CYAN}$1:${DF_NC}"; }
}
# ============================================================================
# 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
# Preview without sudo - only show info accessible to current user
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,args 2>/dev/null || echo "Process may have exited"
echo ""
echo "=== Command Line ==="
cat /proc/$pid/cmdline 2>/dev/null | tr "\0" " " || echo "(not accessible)"
echo ""
echo "=== Working Directory ==="
readlink /proc/$pid/cwd 2>/dev/null || echo "(not accessible)"
echo ""
echo "=== File Descriptors (owned by you) ==="
ls -l /proc/$pid/fd 2>/dev/null | head -10 || echo "(not accessible)"' \
--preview-window=right:50%:wrap \
--header="Process list | Enter: actions | (sorted by memory)")
[[ -z "$selected" ]] && return
local pid=$(echo "$selected" | awk '{print $2}')
local user=$(echo "$selected" | awk '{print $1}')
local cmd=$(echo "$selected" | awk '{for(i=11;i<=NF;i++) printf $i" "; print ""}')
echo ""
df_print_section "Selected Process"
echo " PID: $pid"
echo " User: $user"
echo " CMD: ${cmd:0:60}"
echo ""
echo "Actions:"
echo " 1) Show full details"
echo " 2) Send SIGTERM (graceful stop)"
echo " 3) Send SIGKILL (force kill)"
echo " 4) Send SIGHUP (reload config)"
echo " 5) Open files (requires sudo)"
echo " 6) Cancel"
echo ""
read -k1 "action?Action [1]: "
echo ""
case "${action:-1}" in
1)
echo ""
ps -p "$pid" -f
echo ""
echo "Command line:"
cat /proc/$pid/cmdline 2>/dev/null | tr '\0' ' '
echo ""
;;
2)
df_print_step "Sending SIGTERM to $pid..."
if kill -TERM "$pid" 2>/dev/null; then
df_print_success "Signal sent"
else
df_print_warning "Failed - trying with sudo..."
sudo kill -TERM "$pid" && df_print_success "Signal sent" || df_print_error "Failed"
fi
;;
3)
df_print_step "Sending SIGKILL to $pid..."
if kill -KILL "$pid" 2>/dev/null; then
df_print_success "Signal sent"
else
df_print_warning "Failed - trying with sudo..."
sudo kill -KILL "$pid" && df_print_success "Signal sent" || df_print_error "Failed"
fi
;;
4)
df_print_step "Sending SIGHUP to $pid..."
if kill -HUP "$pid" 2>/dev/null; then
df_print_success "Signal sent"
else
df_print_warning "Failed - trying with sudo..."
sudo kill -HUP "$pid" && df_print_success "Signal sent" || df_print_error "Failed"
fi
;;
5)
echo ""
df_print_step "Open files for PID $pid:"
sudo lsof -p "$pid" 2>/dev/null | head -30 || df_print_error "Failed to get open files"
;;
*)
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 \
--preview='pid=$(echo {} | awk "{print \$2}");
ps -p $pid -o pid,ppid,user,%cpu,%mem,stat,args 2>/dev/null' \
--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:0:40})"
if kill "$pid" 2>/dev/null; then
df_print_success "Killed"
else
df_print_warning "Trying with sudo..."
sudo kill "$pid" && df_print_success "Killed" || df_print_error "Failed"
fi
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() {
cat << 'EOF'
FZF Utilities
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'