Dotfiles update 2025-12-25 10:23

This commit is contained in:
Aaron D. Lee
2025-12-25 10:23:24 -05:00
parent 272c5e3374
commit 492c391cd0
10 changed files with 653 additions and 2616 deletions

View File

@@ -2,429 +2,209 @@
# Btrfs Helpers for Arch/CachyOS # Btrfs Helpers for Arch/CachyOS
# ============================================================================ # ============================================================================
# Quick commands for btrfs filesystem management # Quick commands for btrfs filesystem management
# CachyOS defaults to btrfs, so these are highly useful
#
# Commands:
# btrfs-usage - Show filesystem usage
# btrfs-subs - List subvolumes
# btrfs-balance - Start balance operation
# btrfs-scrub - Start/check scrub
# btrfs-defrag - Defragment file or directory
# btrfs-compress - Show compression stats
# btrfs-info - Full filesystem info
# btrfs-health - Quick health check
# ============================================================================ # ============================================================================
# Source shared colors (with fallback) source "${0:A:h}/../lib/utils.zsh" 2>/dev/null || \
source "${0:A:h}/../lib/colors.zsh" 2>/dev/null || \ source "$HOME/.dotfiles/zsh/lib/utils.zsh" 2>/dev/null
source "$HOME/.dotfiles/zsh/lib/colors.zsh" 2>/dev/null || {
typeset -g DF_GREEN=$'\033[0;32m' DF_YELLOW=$'\033[1;33m'
typeset -g DF_RED=$'\033[0;31m' DF_BLUE=$'\033[0;34m'
typeset -g DF_CYAN=$'\033[0;36m' DF_NC=$'\033[0m'
}
# ============================================================================
# Configuration
# ============================================================================
typeset -g BTRFS_DEFAULT_MOUNT="${BTRFS_DEFAULT_MOUNT:-/}" typeset -g BTRFS_DEFAULT_MOUNT="${BTRFS_DEFAULT_MOUNT:-/}"
# ============================================================================
# Detection
# ============================================================================
_btrfs_check() { _btrfs_check() {
if ! command -v btrfs &>/dev/null; then df_require_cmd btrfs btrfs-progs || return 1
echo -e "${DF_RED}${DF_NC} btrfs-progs not installed" if ! df_is_btrfs; then
echo "Install: sudo pacman -S btrfs-progs" df_print_warning "Root filesystem is not btrfs"
return 1 return 1
fi fi
# Check if root is btrfs
local fstype=$(df -T / | awk 'NR==2 {print $2}')
if [[ "$fstype" != "btrfs" ]]; then
echo -e "${DF_YELLOW}${DF_NC} Root filesystem is not btrfs (detected: $fstype)"
return 1
fi
return 0 return 0
} }
# ============================================================================
# Core Commands
# ============================================================================
# Show filesystem usage
btrfs-usage() { btrfs-usage() {
_btrfs_check || return 1 _btrfs_check || return 1
local mount="${1:-$BTRFS_DEFAULT_MOUNT}" local mount="${1:-$BTRFS_DEFAULT_MOUNT}"
df_print_func_name "Btrfs Filesystem Usage: ${mount}" df_print_func_name "Btrfs Filesystem Usage: ${mount}"
sudo btrfs filesystem usage "$mount" -h sudo btrfs filesystem usage "$mount" -h
} }
# List all subvolumes
btrfs-subs() { btrfs-subs() {
_btrfs_check || return 1 _btrfs_check || return 1
local mount="${1:-$BTRFS_DEFAULT_MOUNT}" local mount="${1:-$BTRFS_DEFAULT_MOUNT}"
df_print_func_name "Btrfs Subvolumes" df_print_func_name "Btrfs Subvolumes"
df_print_section "Subvolume List"
echo -e "${DF_CYAN}Subvolume List:${DF_NC}"
sudo btrfs subvolume list "$mount" | while read -r line; do sudo btrfs subvolume list "$mount" | while read -r line; do
local path=$(echo "$line" | awk '{print $NF}') local path=$(echo "$line" | awk '{print $NF}')
local id=$(echo "$line" | awk '{print $2}') local id=$(echo "$line" | awk '{print $2}')
echo -e " ${DF_GREEN}${DF_NC} [$id] $path" df_print_indent " [$id] $path"
done done
echo "" echo ""
echo -e "${DF_CYAN}Default Subvolume:${DF_NC}" df_print_section "Default Subvolume"
sudo btrfs subvolume get-default "$mount" sudo btrfs subvolume get-default "$mount"
} }
# Start balance operation
btrfs-balance() { btrfs-balance() {
_btrfs_check || return 1 _btrfs_check || return 1
local mount="${1:-$BTRFS_DEFAULT_MOUNT}" local mount="${1:-$BTRFS_DEFAULT_MOUNT}"
local usage="${2:-50}" # Default: rebalance chunks with <50% usage local usage="${2:-50}"
df_print_func_name "Btrfs Balance" df_print_func_name "Btrfs Balance"
df_confirm_warning "This may take a while and use significant I/O" || return 0
echo -e "${DF_YELLOW}${DF_NC} This may take a while and use significant I/O"
echo "" echo ""
df_print_step "Balancing data chunks with <${usage}% usage..."
read -q "REPLY?Continue? [y/N]: "; echo
[[ ! "$REPLY" =~ ^[Yy]$ ]] && return 0
echo ""
echo -e "${DF_BLUE}==>${DF_NC} Balancing data chunks with <${usage}% usage..."
sudo btrfs balance start -dusage="$usage" -musage="$usage" "$mount" -v sudo btrfs balance start -dusage="$usage" -musage="$usage" "$mount" -v
[[ $? -eq 0 ]] && df_print_success "Balance completed" || df_print_warning "Balance finished (may have been interrupted)"
if [[ $? -eq 0 ]]; then
echo -e "${DF_GREEN}${DF_NC} Balance completed"
else
echo -e "${DF_YELLOW}${DF_NC} Balance finished (may have been interrupted or had no work)"
fi
} }
# Check balance status
btrfs-balance-status() { btrfs-balance-status() {
_btrfs_check || return 1 _btrfs_check || return 1
local mount="${1:-$BTRFS_DEFAULT_MOUNT}"
df_print_func_name "Btrfs Balance Status" df_print_func_name "Btrfs Balance Status"
sudo btrfs balance status "${1:-$BTRFS_DEFAULT_MOUNT}"
sudo btrfs balance status "$mount"
} }
# Cancel running balance
btrfs-balance-cancel() { btrfs-balance-cancel() {
_btrfs_check || return 1 _btrfs_check || return 1
local mount="${1:-$BTRFS_DEFAULT_MOUNT}" df_print_step "Cancelling balance..."
sudo btrfs balance cancel "${1:-$BTRFS_DEFAULT_MOUNT}"
echo -e "${DF_BLUE}==>${DF_NC} Cancelling balance on ${mount}..."
sudo btrfs balance cancel "$mount"
} }
# Start scrub operation
btrfs-scrub() { btrfs-scrub() {
_btrfs_check || return 1 _btrfs_check || return 1
local mount="${1:-$BTRFS_DEFAULT_MOUNT}" local mount="${1:-$BTRFS_DEFAULT_MOUNT}"
df_print_func_name "Btrfs Scrub" df_print_func_name "Btrfs Scrub"
# Check if scrub is already running
local status=$(sudo btrfs scrub status "$mount" 2>/dev/null) local status=$(sudo btrfs scrub status "$mount" 2>/dev/null)
if echo "$status" | grep -q "running"; then if echo "$status" | grep -q "running"; then
echo -e "${DF_CYAN}Scrub Status (running):${DF_NC}" df_print_section "Scrub Status (running)"
echo "$status" | sed 's/^/ /' echo "$status" | sed 's/^/ /'
return 0 return 0
fi fi
df_print_warning "Scrub verifies data integrity and may take hours"
echo -e "${DF_YELLOW}${DF_NC} Scrub verifies data integrity and may take hours" df_confirm "Start scrub?" || return 0
read -q "REPLY?Start scrub? [y/N]: "; echo df_print_step "Starting scrub..."
[[ ! "$REPLY" =~ ^[Yy]$ ]] && return 0
echo ""
echo -e "${DF_BLUE}==>${DF_NC} Starting scrub..."
sudo btrfs scrub start "$mount" sudo btrfs scrub start "$mount"
echo "" echo ""
echo -e "${DF_CYAN}Scrub Status:${DF_NC}" df_print_section "Scrub Status"
sudo btrfs scrub status "$mount" sudo btrfs scrub status "$mount"
df_print_info "Monitor with: btrfs-scrub-status"
echo ""
echo -e "${DF_CYAN}Monitor with:${DF_NC} btrfs-scrub-status"
} }
# Show scrub status
btrfs-scrub-status() { btrfs-scrub-status() {
_btrfs_check || return 1 _btrfs_check || return 1
local mount="${1:-$BTRFS_DEFAULT_MOUNT}"
df_print_func_name "Btrfs Scrub Status" df_print_func_name "Btrfs Scrub Status"
sudo btrfs scrub status "${1:-$BTRFS_DEFAULT_MOUNT}"
sudo btrfs scrub status "$mount"
} }
# Cancel scrub
btrfs-scrub-cancel() { btrfs-scrub-cancel() {
_btrfs_check || return 1 _btrfs_check || return 1
local mount="${1:-$BTRFS_DEFAULT_MOUNT}" df_print_step "Cancelling scrub..."
sudo btrfs scrub cancel "${1:-$BTRFS_DEFAULT_MOUNT}"
echo -e "${DF_BLUE}==>${DF_NC} Cancelling scrub on ${mount}..."
sudo btrfs scrub cancel "$mount"
} }
# Defragment file or directory
btrfs-defrag() { btrfs-defrag() {
_btrfs_check || return 1 _btrfs_check || return 1
local target="${1:-.}" local target="${1:-.}"
[[ ! -e "$target" ]] && { df_print_error "Target not found: $target"; return 1; }
if [[ ! -e "$target" ]]; then
echo -e "${DF_RED}${DF_NC} Target not found: $target"
return 1
fi
df_print_func_name "Btrfs Defragment" df_print_func_name "Btrfs Defragment"
if [[ -d "$target" ]]; then if [[ -d "$target" ]]; then
echo -e "${DF_YELLOW}${DF_NC} Recursive defrag on directory: $target" df_print_warning "Recursive defrag on directory: $target"
read -q "REPLY?Continue? [y/N]: "; echo df_confirm "Continue?" || return 0
[[ ! "$REPLY" =~ ^[Yy]$ ]] && return 0
sudo btrfs filesystem defragment -r -v "$target" sudo btrfs filesystem defragment -r -v "$target"
else else
echo -e "${DF_BLUE}==>${DF_NC} Defragmenting: $target" df_print_step "Defragmenting: $target"
sudo btrfs filesystem defragment -v "$target" sudo btrfs filesystem defragment -v "$target"
fi fi
df_print_success "Defragmentation complete"
echo -e "${DF_GREEN}${DF_NC} Defragmentation complete"
} }
# Show compression stats (requires compsize)
btrfs-compress() { btrfs-compress() {
_btrfs_check || return 1 _btrfs_check || return 1
local target="${1:-$BTRFS_DEFAULT_MOUNT}" df_require_cmd compsize || return 1
if ! command -v compsize &>/dev/null; then
echo -e "${DF_YELLOW}${DF_NC} compsize not installed"
echo "Install: sudo pacman -S compsize"
return 1
fi
df_print_func_name "Btrfs Compression Statistics" df_print_func_name "Btrfs Compression Statistics"
sudo compsize "${1:-$BTRFS_DEFAULT_MOUNT}"
sudo compsize "$target"
} }
# ============================================================================
# Information Commands
# ============================================================================
# Full filesystem info
btrfs-info() { btrfs-info() {
_btrfs_check || return 1 _btrfs_check || return 1
local mount="${1:-$BTRFS_DEFAULT_MOUNT}" local mount="${1:-$BTRFS_DEFAULT_MOUNT}"
df_print_func_name "Btrfs Filesystem Information" df_print_func_name "Btrfs Filesystem Information"
df_print_section "Filesystem Show"
echo -e "${DF_CYAN}Filesystem Show:${DF_NC}"
sudo btrfs filesystem show "$mount" sudo btrfs filesystem show "$mount"
echo -e "\n${DF_CYAN}Filesystem df:${DF_NC}"
sudo btrfs filesystem df "$mount"
echo -e "\n${DF_CYAN}Device Stats:${DF_NC}"
sudo btrfs device stats "$mount"
echo "" echo ""
df_print_section "Filesystem df"
sudo btrfs filesystem df "$mount"
echo ""
df_print_section "Device Stats"
sudo btrfs device stats "$mount"
} }
# Quick health check
btrfs-health() { btrfs-health() {
_btrfs_check || return 1 _btrfs_check || return 1
local mount="${1:-$BTRFS_DEFAULT_MOUNT}" local mount="${1:-$BTRFS_DEFAULT_MOUNT}"
df_print_func_name "Btrfs Health Check" df_print_func_name "Btrfs Health Check"
local issues=0 local issues=0
# Check device stats for errors df_print_section "Device Errors"
echo -e "${DF_CYAN}Device Errors:${DF_NC}" local errors=$(sudo btrfs device stats "$mount" 2>/dev/null | grep -v " 0$" | grep -v "^$")
local stats=$(sudo btrfs device stats "$mount" 2>/dev/null) [[ -z "$errors" ]] && df_print_indent "✓ No errors" || { df_print_indent "✗ Errors detected:"; echo "$errors" | sed 's/^/ /'; ((issues++)); }
local errors=$(echo "$stats" | grep -v " 0$" | grep -v "^$")
if [[ -z "$errors" ]]; then
echo -e " ${DF_GREEN}${DF_NC} No device errors detected"
else
echo -e " ${DF_RED}${DF_NC} Errors detected:"
echo "$errors" | sed 's/^/ /'
issues=$((issues + 1))
fi
# Check allocation
echo -e "\n${DF_CYAN}Space Allocation:${DF_NC}"
local usage=$(sudo btrfs filesystem usage "$mount" -b 2>/dev/null)
local used_pct=$(echo "$usage" | grep "Used:" | head -1 | awk '{print $2}' | tr -d '%')
echo ""
df_print_section "Space Allocation"
local used_pct=$(sudo btrfs filesystem usage "$mount" -b 2>/dev/null | grep "Used:" | head -1 | awk '{print $2}' | tr -d '%')
if [[ -n "$used_pct" ]]; then if [[ -n "$used_pct" ]]; then
if (( used_pct >= 90 )); then (( used_pct >= 90 )) && { df_print_indent "${used_pct}% full - critical!"; ((issues++)); } || \
echo -e " ${DF_RED}${DF_NC} Filesystem ${used_pct}% full - critical!" (( used_pct >= 80 )) && df_print_indent " ${used_pct}% full" || df_print_indent " ${used_pct}% used"
issues=$((issues + 1))
elif (( used_pct >= 80 )); then
echo -e " ${DF_YELLOW}${DF_NC} Filesystem ${used_pct}% full - consider cleanup"
else
echo -e " ${DF_GREEN}${DF_NC} Filesystem ${used_pct}% used"
fi
fi
# Check last scrub
echo -e "\n${DF_CYAN}Last Scrub:${DF_NC}"
local scrub_status=$(sudo btrfs scrub status "$mount" 2>/dev/null)
local scrub_date=$(echo "$scrub_status" | grep "Scrub started" | awk '{print $3, $4, $5}')
local scrub_errors=$(echo "$scrub_status" | grep "Error summary" | grep -v "no errors")
if [[ -n "$scrub_date" ]]; then
echo -e " Last scrub: $scrub_date"
if [[ -n "$scrub_errors" ]]; then
echo -e " ${DF_RED}${DF_NC} Scrub found errors"
echo "$scrub_errors" | sed 's/^/ /'
issues=$((issues + 1))
else
echo -e " ${DF_GREEN}${DF_NC} No errors in last scrub"
fi
else
echo -e " ${DF_YELLOW}${DF_NC} No scrub has been run (recommended monthly)"
fi
# Summary
echo ""
if (( issues == 0 )); then
echo -e "${DF_GREEN}${DF_NC} Btrfs filesystem appears healthy"
else
echo -e "${DF_RED}${DF_NC} Found $issues issue(s) - investigate above"
fi fi
echo "" echo ""
df_print_section "Last Scrub"
local scrub=$(sudo btrfs scrub status "$mount" 2>/dev/null)
local scrub_date=$(echo "$scrub" | grep "Scrub started" | awk '{print $3, $4, $5}')
[[ -n "$scrub_date" ]] && df_print_indent "Last: $scrub_date" || df_print_indent "⚠ No scrub run yet"
echo ""
(( issues == 0 )) && df_print_success "Filesystem healthy" || df_print_error "Found $issues issue(s)"
} }
# ============================================================================
# Snapshot Helpers (complement snapper.zsh)
# ============================================================================
# Show snapshot space usage
btrfs-snap-usage() { btrfs-snap-usage() {
_btrfs_check || return 1 _btrfs_check || return 1
df_print_func_name "Snapshot Disk Space Usage" df_print_func_name "Snapshot Disk Space Usage"
if [[ -d "/.snapshots" ]]; then if [[ -d "/.snapshots" ]]; then
echo -e "${DF_CYAN}Snapshot Directory:${DF_NC}" df_print_section "Snapshot Directory"
local size local size=$(timeout 10 sudo du -sh /.snapshots 2>/dev/null | cut -f1)
size=$(sudo du -sh /.snapshots 2>/dev/null | cut -f1) df_print_indent "${size:-Unable to calculate}"
if [[ -n "$size" ]]; then
echo " $size"
else
echo " Unable to calculate (timeout or error)"
fi
echo -e "\n${DF_CYAN}Individual Snapshots (top 10 by size):${DF_NC}"
sudo du -sh /.snapshots/*/ 2>/dev/null | sort -h | tail -10 | sed 's/^/ /' || \
echo " Unable to list snapshots"
else
echo -e "${DF_YELLOW}${DF_NC} No /.snapshots directory found"
fi
echo "" echo ""
df_print_section "Individual Snapshots (top 10)"
timeout 30 sudo du -sh /.snapshots/*/ 2>/dev/null | sort -h | tail -10 | sed 's/^/ /'
else
df_print_warning "No /.snapshots directory found"
fi
} }
# ============================================================================
# Maintenance
# ============================================================================
# Full maintenance routine
btrfs-maintain() { btrfs-maintain() {
_btrfs_check || return 1 _btrfs_check || return 1
local mount="${1:-$BTRFS_DEFAULT_MOUNT}" local mount="${1:-$BTRFS_DEFAULT_MOUNT}"
df_print_func_name "Btrfs Maintenance Routine" df_print_func_name "Btrfs Maintenance Routine"
echo "This will: health check, balance, scrub"
echo "This will perform:" df_confirm_warning "This may take several hours" || return 0
echo " 1. Health check" df_print_step "Step 1/3: Health Check"
echo " 2. Balance (low usage chunks)"
echo " 3. Scrub (data integrity)"
echo ""
echo -e "${DF_YELLOW}${DF_NC} This may take several hours"
read -q "REPLY?Continue? [y/N]: "; echo
[[ ! "$REPLY" =~ ^[Yy]$ ]] && return 0
echo ""
echo -e "${DF_BLUE}==>${DF_NC} Step 1/3: Health Check"
btrfs-health "$mount" btrfs-health "$mount"
df_print_step "Step 2/3: Balance"
echo -e "${DF_BLUE}==>${DF_NC} Step 2/3: Balance"
sudo btrfs balance start -dusage=50 -musage=50 "$mount" sudo btrfs balance start -dusage=50 -musage=50 "$mount"
df_print_step "Step 3/3: Scrub"
echo "" sudo btrfs scrub start -B "$mount"
echo -e "${DF_BLUE}==>${DF_NC} Step 3/3: Scrub" df_print_success "Maintenance complete"
sudo btrfs scrub start -B "$mount" # -B runs in foreground
echo ""
echo -e "${DF_GREEN}${DF_NC} Maintenance complete"
btrfs-health "$mount"
} }
# ============================================================================
# Aliases
# ============================================================================
alias btru='btrfs-usage'
alias btrs='btrfs-subs'
alias btrh='btrfs-health'
alias btri='btrfs-info'
alias btrc='btrfs-compress'
# ============================================================================
# Help
# ============================================================================
btrfs-help() { btrfs-help() {
df_print_func_name "Btrfs Helper Commands" df_print_func_name "Btrfs Helper Commands"
cat << 'EOF' cat << 'EOF'
btrfs-usage [mount] Filesystem usage
Information: btrfs-subs [mount] List subvolumes
btrfs-usage [mount] Filesystem usage summary btrfs-info [mount] Full filesystem info
btrfs-subs [mount] List all subvolumes
btrfs-info [mount] Full filesystem information
btrfs-health [mount] Quick health check btrfs-health [mount] Quick health check
btrfs-compress [path] Compression statistics (requires compsize) btrfs-compress [path] Compression stats
btrfs-balance [mount] Start balance
Maintenance: btrfs-scrub [mount] Start scrub
btrfs-balance [mount] Start balance operation btrfs-defrag <path> Defragment
btrfs-balance-status Check balance progress btrfs-snap-usage Snapshot space usage
btrfs-balance-cancel Cancel running balance btrfs-maintain [mount] Full maintenance
btrfs-scrub [mount] Start scrub (integrity check)
btrfs-scrub-status Check scrub progress
btrfs-scrub-cancel Cancel running scrub
btrfs-defrag <path> Defragment file/directory
btrfs-maintain [mount] Full maintenance routine
Snapshots:
btrfs-snap-usage Show snapshot space usage
Aliases:
btru btrfs-usage
btrs btrfs-subs
btrh btrfs-health
btri btrfs-info
btrc btrfs-compress
Note: Most commands default to / if no mount point specified.
See also: snapper.zsh for snapshot management
EOF EOF
} }
alias btru='btrfs-usage' btrs='btrfs-subs' btrh='btrfs-health' btri='btrfs-info' btrc='btrfs-compress'

View File

@@ -2,322 +2,135 @@
# Command Palette - Fuzzy Command Launcher for Zsh # Command Palette - Fuzzy Command Launcher for Zsh
# ============================================================================ # ============================================================================
# A Raycast/Alfred-style command palette for the terminal # A Raycast/Alfred-style command palette for the terminal
#
# Features:
# - Search aliases, functions, recent commands
# - Search bookmarked directories
# - Search dotfiles scripts
# - Quick actions (edit config, reload shell, etc.)
#
# Keybinding: Ctrl+Space (configurable) # Keybinding: Ctrl+Space (configurable)
#
# Requirements: fzf
# ============================================================================ # ============================================================================
# Source shared colors (with fallback) source "${0:A:h}/../lib/utils.zsh" 2>/dev/null || \
source "${0:A:h}/../lib/colors.zsh" 2>/dev/null || \ source "$HOME/.dotfiles/zsh/lib/utils.zsh" 2>/dev/null
source "$HOME/.dotfiles/zsh/lib/colors.zsh" 2>/dev/null || {
typeset -g DF_GREEN=$'\033[0;32m' DF_BLUE=$'\033[0;34m'
typeset -g DF_CYAN=$'\033[0;36m' DF_NC=$'\033[0m'
}
# ============================================================================ typeset -g PALETTE_HOTKEY="${PALETTE_HOTKEY:-^@}"
# Configuration
# ============================================================================
typeset -g PALETTE_HOTKEY="${PALETTE_HOTKEY:-^@}" # Ctrl+Space
typeset -g PALETTE_HISTORY_SIZE=50 typeset -g PALETTE_HISTORY_SIZE=50
typeset -g PALETTE_BOOKMARKS_FILE="$HOME/.dotfiles/.bookmarks" typeset -g PALETTE_BOOKMARKS_FILE="$HOME/.dotfiles/.bookmarks"
typeset -g DOTFILES_DIR="${DOTFILES_DIR:-$HOME/.dotfiles}" typeset -g DOTFILES_DIR="${DOTFILES_DIR:-$HOME/.dotfiles}"
# Icons (works with most terminals) typeset -g ICON_ALIAS="⚡" ICON_FUNC="λ" ICON_HIST="↺" ICON_DIR="📁"
typeset -g ICON_ALIAS="⚡" typeset -g ICON_SCRIPT="⚙" ICON_ACTION="★" ICON_GIT="⎇"
typeset -g ICON_FUNC="λ"
typeset -g ICON_HIST="↺"
typeset -g ICON_DIR="📁"
typeset -g ICON_SCRIPT="⚙"
typeset -g ICON_ACTION="★"
typeset -g ICON_GIT="⎇"
typeset -g ICON_DOCKER="◉"
typeset -g ICON_EDIT="✎"
typeset -g ICON_RUN="▶"
# ============================================================================
# Check Dependencies
# ============================================================================
_palette_check_deps() {
if ! command -v fzf &>/dev/null; then
echo "Command palette requires fzf."
echo "Install: git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf && ~/.fzf/install"
return 1
fi
return 0
}
# ============================================================================
# Data Sources
# ============================================================================
_palette_get_aliases() { _palette_get_aliases() {
alias | sed 's/^alias //' | while IFS='=' read -r alias_name cmd; do alias | sed 's/^alias //' | while IFS='=' read -r name cmd; do
cmd="${cmd#\'}" cmd="${cmd#\'}"; cmd="${cmd%\'}"; cmd="${cmd#\"}"; cmd="${cmd%\"}"
cmd="${cmd%\'}" printf "%s\t%s\t%s\t%s\n" "$ICON_ALIAS" "alias" "$name" "$cmd"
cmd="${cmd#\"}"
cmd="${cmd%\"}"
printf "%s\t%s\t%s\t%s\n" "$ICON_ALIAS" "alias" "$alias_name" "$cmd"
done done
} }
_palette_get_functions() { _palette_get_functions() {
print -l ${(ok)functions} | grep -v '^_' | while read -r func_name; do print -l ${(ok)functions} | grep -v "^_" | while read -r name; do
printf "%s\t%s\t%s\t%s\n" "$ICON_FUNC" "func" "$func_name" "function" printf "%s\t%s\t%s\t%s\n" "$ICON_FUNC" "function" "$name" ""
done done
} }
_palette_get_history() { _palette_get_history() {
fc -ln -$PALETTE_HISTORY_SIZE | tac | awk '!seen[$0]++' | head -30 | while read -r cmd; do fc -ln -"$PALETTE_HISTORY_SIZE" 2>/dev/null | awk '!seen[$0]++' | while read -r cmd; do
[[ -n "$cmd" ]] && printf "%s\t%s\t%s\t%s\n" "$ICON_HIST" "history" "${cmd:0:50}" "$cmd" [[ -n "$cmd" ]] && printf "%s\t%s\t%s\t%s\n" "$ICON_HIST" "history" "$cmd" ""
done done
} }
_palette_get_bookmarks() { _palette_get_bookmarks() {
[[ ! -f "$PALETTE_BOOKMARKS_FILE" ]] && return [[ ! -f "$PALETTE_BOOKMARKS_FILE" ]] && return
while IFS='|' read -r name path desc; do
while IFS='|' read -r bm_name bm_path; do [[ "$name" =~ ^# || -z "$name" ]] && continue
[[ -n "$bm_name" && -n "$bm_path" ]] && printf "%s\t%s\t%s\t%s\n" "$ICON_DIR" "bookmark" "$bm_name" "cd $bm_path" printf "%s\t%s\t%s\t%s\n" "$ICON_DIR" "bookmark" "$name" "$path"
done < "$PALETTE_BOOKMARKS_FILE" done < "$PALETTE_BOOKMARKS_FILE"
} }
_palette_get_scripts() {
[[ ! -d "$DOTFILES_DIR/bin" ]] && return
for script in "$DOTFILES_DIR/bin"/*.sh; do
[[ -f "$script" ]] || continue
local script_name=$(basename "$script" .sh)
printf "%s\t%s\t%s\t%s\n" "$ICON_SCRIPT" "script" "$script_name" "$script"
done
}
_palette_get_git_commands() {
git rev-parse --git-dir &>/dev/null || return
local branch=$(git branch --show-current 2>/dev/null)
printf "%s\t%s\t%s\t%s\n" "$ICON_GIT" "git" "status" "git status"
printf "%s\t%s\t%s\t%s\n" "$ICON_GIT" "git" "pull $branch" "git pull origin $branch"
printf "%s\t%s\t%s\t%s\n" "$ICON_GIT" "git" "push $branch" "git push origin $branch"
printf "%s\t%s\t%s\t%s\n" "$ICON_GIT" "git" "diff" "git diff"
printf "%s\t%s\t%s\t%s\n" "$ICON_GIT" "git" "log" "git log --oneline -20"
printf "%s\t%s\t%s\t%s\n" "$ICON_GIT" "git" "stash" "git stash"
printf "%s\t%s\t%s\t%s\n" "$ICON_GIT" "git" "stash pop" "git stash pop"
}
_palette_get_docker_commands() {
command -v docker &>/dev/null || return
printf "%s\t%s\t%s\t%s\n" "$ICON_DOCKER" "docker" "ps" "docker ps"
printf "%s\t%s\t%s\t%s\n" "$ICON_DOCKER" "docker" "ps -a" "docker ps -a"
printf "%s\t%s\t%s\t%s\n" "$ICON_DOCKER" "docker" "images" "docker images"
printf "%s\t%s\t%s\t%s\n" "$ICON_DOCKER" "docker" "compose up" "docker-compose up -d"
printf "%s\t%s\t%s\t%s\n" "$ICON_DOCKER" "docker" "compose down" "docker-compose down"
printf "%s\t%s\t%s\t%s\n" "$ICON_DOCKER" "docker" "prune" "docker system prune -af"
}
_palette_get_actions() { _palette_get_actions() {
printf "%s\t%s\t%s\t%s\n" "$ICON_ACTION" "action" "Reload shell" "exec zsh" cat << 'EOF'
printf "%s\t%s\t%s\t%s\n" "$ICON_EDIT" "action" "Edit .zshrc" "${EDITOR:-vim} ~/.zshrc" ★ action reload-shell Reload zsh configuration
printf "%s\t%s\t%s\t%s\n" "$ICON_EDIT" "action" "Edit dotfiles.conf" "${EDITOR:-vim} $DOTFILES_DIR/dotfiles.conf" ★ action edit-zshrc Edit ~/.zshrc
printf "%s\t%s\t%s\t%s\n" "$ICON_EDIT" "action" "Edit theme" "${EDITOR:-vim} $DOTFILES_DIR/zsh/themes/adlee.zsh-theme" ★ action dotfiles-update Update dotfiles
printf "%s\t%s\t%s\t%s\n" "$ICON_SCRIPT" "action" "Dotfiles doctor" "dfd" EOF
printf "%s\t%s\t%s\t%s\n" "$ICON_SCRIPT" "action" "Dotfiles sync" "dfs" df_in_git_repo && cat << 'EOF'
printf "%s\t%s\t%s\t%s\n" "$ICON_SCRIPT" "action" "Shell stats" "dfstats" ⎇ git git-status Show git status
printf "%s\t%s\t%s\t%s\n" "$ICON_SCRIPT" "action" "Compile zsh" "dfcompile" ⎇ git git-pull Pull latest
printf "%s\t%s\t%s\t%s\n" "$ICON_SCRIPT" "action" "Vault list" "vault list" ⎇ git git-push Push commits
printf "%s\t%s\t%s\t%s\n" "$ICON_ACTION" "action" "Clear screen" "clear" EOF
printf "%s\t%s\t%s\t%s\n" "$ICON_DIR" "action" "Home" "cd ~"
printf "%s\t%s\t%s\t%s\n" "$ICON_DIR" "action" "Dotfiles" "cd $DOTFILES_DIR"
printf "%s\t%s\t%s\t%s\n" "$ICON_DIR" "action" "Projects" "cd ~/projects 2>/dev/null || cd ~"
} }
_palette_get_directories() { _palette_run_action() {
dirs -v 2>/dev/null | tail -n +2 | head -10 | while read -r num dir; do case "$1" in
[[ -n "$dir" ]] && printf "%s\t%s\t%s\t%s\n" "$ICON_DIR" "recent" "$dir" "cd $dir" reload-shell) source ~/.zshrc; df_print_success "Shell reloaded" ;;
done edit-zshrc) ${EDITOR:-vim} ~/.zshrc ;;
} dotfiles-update) cd "$DOTFILES_DIR" && git pull ;;
git-status) git status ;;
# ============================================================================ git-pull) git pull ;;
# Main Palette Function git-push) git push ;;
# ============================================================================ *) df_print_error "Unknown action: $1" ;;
_palette_generate_entries() {
_palette_get_actions
_palette_get_git_commands
_palette_get_docker_commands
_palette_get_aliases
_palette_get_bookmarks
_palette_get_scripts
_palette_get_directories
_palette_get_history
_palette_get_functions
}
command_palette() {
_palette_check_deps || return 1
local selection
selection=$(_palette_generate_entries | \
fzf --height=60% \
--layout=reverse \
--border=rounded \
--prompt=' ' \
--pointer='▶' \
--header='Command Palette (ESC to cancel)' \
--preview-window=hidden \
--delimiter=$'\t' \
--with-nth=1,3 \
--tabstop=2 \
--ansi \
--bind='ctrl-r:reload(_palette_generate_entries)' \
--expect=ctrl-e,ctrl-y)
[[ -z "$selection" ]] && return
local key=$(echo "$selection" | head -1)
local line=$(echo "$selection" | tail -1)
local cmd=$(echo "$line" | cut -f4)
[[ -z "$cmd" ]] && return
case "$key" in
ctrl-e)
print -z "$cmd"
;;
ctrl-y)
echo -n "$cmd" | pbcopy 2>/dev/null || echo -n "$cmd" | xclip -selection clipboard 2>/dev/null
echo "Copied: $cmd"
;;
*)
echo " $cmd"
eval "$cmd"
;;
esac esac
} }
# Alias for easier access palette() {
palette() { command_palette; } df_require_cmd fzf || return 1
p() { command_palette; } local items=$(_palette_get_actions; _palette_get_aliases; _palette_get_functions; _palette_get_bookmarks; _palette_get_history)
local sel=$(echo "$items" | fzf --ansi --delimiter='\t' --with-nth=1,3,4 $(df_fzf_opts) --prompt='> ')
# ============================================================================ [[ -z "$sel" ]] && return
# Bookmark Management local type=$(echo "$sel" | cut -f2) name=$(echo "$sel" | cut -f3) detail=$(echo "$sel" | cut -f4)
# ============================================================================ case "$type" in
alias|history) print -z "$name" ;;
function) print -z "$name " ;;
bookmark) cd "$detail" && pwd ;;
action|git) _palette_run_action "$name" ;;
esac
}
bookmark() { bookmark() {
local bm_name="$1" local cmd="${1:-list}"; shift 2>/dev/null
local bm_path="${2:-$(pwd)}" case "$cmd" in
add)
# Ensure bookmarks file parent directory exists local name="$1" path="${2:-$(pwd)}" desc="$3"
mkdir -p "$(dirname "$PALETTE_BOOKMARKS_FILE")" 2>/dev/null [[ -z "$name" ]] && { echo "Usage: bookmark add <name> [path]"; return 1; }
df_ensure_file "$PALETTE_BOOKMARKS_FILE" "# Bookmarks: name|path|description"
# Create bookmarks file if it doesn't exist grep -q "^${name}|" "$PALETTE_BOOKMARKS_FILE" 2>/dev/null && {
[[ ! -f "$PALETTE_BOOKMARKS_FILE" ]] && touch "$PALETTE_BOOKMARKS_FILE" df_confirm "Overwrite '$name'?" || return 1
grep -v "^${name}|" "$PALETTE_BOOKMARKS_FILE" > "${PALETTE_BOOKMARKS_FILE}.tmp"
if [[ -z "$bm_name" ]]; then mv "${PALETTE_BOOKMARKS_FILE}.tmp" "$PALETTE_BOOKMARKS_FILE"
echo "Usage: bookmark <name> [path]" }
echo " bookmark list" echo "${name}|${path}|${desc}" >> "$PALETTE_BOOKMARKS_FILE"
echo " bookmark delete <name>" df_print_success "Bookmarked: $name$path"
return 1
fi
case "$bm_name" in
list|ls)
df_print_func_name "Bookmarks"
if [[ -s "$PALETTE_BOOKMARKS_FILE" ]]; then
while IFS='|' read -r stored_name stored_path || [[ -n "$stored_name" ]]; do
[[ -n "$stored_name" ]] && echo -e " ${DF_GREEN}${DF_NC} $stored_name$stored_path"
done < "$PALETTE_BOOKMARKS_FILE"
else
echo "No bookmarks yet"
fi
;; ;;
delete|rm) delete|rm)
local to_delete="$2" [[ -z "$1" ]] && { echo "Usage: bookmark delete <name>"; return 1; }
if [[ -z "$to_delete" ]]; then grep -q "^${1}|" "$PALETTE_BOOKMARKS_FILE" 2>/dev/null || { df_print_error "Not found: $1"; return 1; }
echo "Specify bookmark to delete" grep -v "^${1}|" "$PALETTE_BOOKMARKS_FILE" > "${PALETTE_BOOKMARKS_FILE}.tmp"
return 1 mv "${PALETTE_BOOKMARKS_FILE}.tmp" "$PALETTE_BOOKMARKS_FILE"
fi df_print_success "Deleted: $1"
if [[ -s "$PALETTE_BOOKMARKS_FILE" ]]; then
# Use a temp file approach that won't hang
local temp_file="${PALETTE_BOOKMARKS_FILE}.tmp.$$"
grep -v "^${to_delete}|" "$PALETTE_BOOKMARKS_FILE" > "$temp_file" 2>/dev/null || true
mv -f "$temp_file" "$PALETTE_BOOKMARKS_FILE"
echo -e "${DF_GREEN}${DF_NC} Deleted: $to_delete"
else
echo "No bookmarks to delete"
fi
;; ;;
*) list|ls)
# Remove existing bookmark with same name (if file has content) df_print_func_name "Bookmarks"
if [[ -s "$PALETTE_BOOKMARKS_FILE" ]]; then [[ ! -f "$PALETTE_BOOKMARKS_FILE" ]] && { df_print_info "No bookmarks"; return; }
local temp_file="${PALETTE_BOOKMARKS_FILE}.tmp.$$" while IFS='|' read -r name path desc; do
grep -v "^${bm_name}|" "$PALETTE_BOOKMARKS_FILE" > "$temp_file" 2>/dev/null || true [[ "$name" =~ ^# || -z "$name" ]] && continue
mv -f "$temp_file" "$PALETTE_BOOKMARKS_FILE" df_print_indent "$name$path"
fi done < "$PALETTE_BOOKMARKS_FILE"
# Add new bookmark
echo "${bm_name}|${bm_path}" >> "$PALETTE_BOOKMARKS_FILE"
echo -e "${DF_GREEN}${DF_NC} Bookmarked: $bm_name$bm_path"
;; ;;
go)
[[ -z "$1" ]] && { echo "Usage: bookmark go <name>"; return 1; }
local path=$(grep "^${1}|" "$PALETTE_BOOKMARKS_FILE" 2>/dev/null | cut -d'|' -f2)
[[ -n "$path" ]] && cd "$path" || df_print_error "Not found: $1"
;;
*) echo "Usage: bookmark <add|delete|list|go>" ;;
esac esac
} }
# Quick jump to bookmark bm() {
jump() { df_require_cmd fzf || return 1
local bm_name="$1" [[ ! -f "$PALETTE_BOOKMARKS_FILE" ]] && { df_print_info "No bookmarks"; return 1; }
local sel=$(grep -v "^#" "$PALETTE_BOOKMARKS_FILE" | grep -v "^$" | \
if [[ -z "$bm_name" ]]; then fzf $(df_fzf_opts) --delimiter='|' --with-nth=1,2 --prompt='Bookmark > ')
# Fuzzy select bookmark [[ -n "$sel" ]] && cd "$(echo "$sel" | cut -d'|' -f2)"
if [[ ! -s "$PALETTE_BOOKMARKS_FILE" ]]; then
echo "No bookmarks"
return 1
fi
local selection=$(cat "$PALETTE_BOOKMARKS_FILE" | \
fzf --height=40% --layout=reverse --delimiter='|' --with-nth=1 \
--preview='echo "Path: $(echo {} | cut -d"|" -f2)"')
if [[ -n "$selection" ]]; then
local jump_path=$(echo "$selection" | cut -d'|' -f2)
cd "$jump_path" && echo "$jump_path"
fi
else
# Direct jump
local jump_path=$(grep "^${bm_name}|" "$PALETTE_BOOKMARKS_FILE" 2>/dev/null | cut -d'|' -f2)
if [[ -n "$jump_path" ]]; then
cd "$jump_path" && echo "$jump_path"
else
echo "Bookmark not found: $bm_name"
fi
fi
} }
# Aliases _palette_widget() { BUFFER=""; zle redisplay; palette; zle reset-prompt; }
bm() { bookmark "$@"; }
j() { jump "$@"; }
# ============================================================================
# Widget for Keybinding
# ============================================================================
_palette_widget() {
command_palette
zle reset-prompt
}
# Register widget
zle -N _palette_widget zle -N _palette_widget
# Bind to Ctrl+Space (^@)
bindkey "$PALETTE_HOTKEY" _palette_widget bindkey "$PALETTE_HOTKEY" _palette_widget
# Alternative binding: Ctrl+P alias p='palette' bml='bookmark list' bma='bookmark add' bmg='bookmark go'
bindkey '^P' _palette_widget

View File

@@ -1,198 +1,84 @@
# ============================================================================ # ============================================================================
# Password Manager Integration for Zsh (LastPass Only) # Password Manager Integration (LastPass CLI)
# ============================================================================
# Unified interface for LastPass CLI
#
# Usage:
# pw list # List all items
# pw get <item> # Get password
# pw otp <item> # Get OTP/TOTP code
# pw search <query> # Search items
# pw copy <item> # Copy password to clipboard
# ============================================================================ # ============================================================================
# Source shared colors (with fallback) source "${0:A:h}/../lib/utils.zsh" 2>/dev/null || \
source "${0:A:h}/../lib/colors.zsh" 2>/dev/null || \ source "$HOME/.dotfiles/zsh/lib/utils.zsh" 2>/dev/null
source "$HOME/.dotfiles/zsh/lib/colors.zsh" 2>/dev/null || {
typeset -g DF_GREEN=$'\033[0;32m' DF_BLUE=$'\033[0;34m'
typeset -g DF_YELLOW=$'\033[1;33m' DF_CYAN=$'\033[0;36m'
typeset -g DF_RED=$'\033[0;31m' DF_NC=$'\033[0m'
}
# ============================================================================ typeset -g PW_CLIP_TIME="${PW_CLIP_TIME:-45}"
# LastPass Functions
# ============================================================================
_lp_ensure_session() { _pw_check() {
df_require_cmd lpass lastpass-cli || return 1
if ! lpass status -q 2>/dev/null; then if ! lpass status -q 2>/dev/null; then
echo "Signing into LastPass..." >&2 df_print_warning "Not logged in"
lpass login "${LASTPASS_EMAIL:-}" df_print_step "Logging in..."
lpass login --trust "${LPASS_USER:-}" || { df_print_error "Login failed"; return 1; }
fi fi
} }
_lp_list() { _pw_copy() {
_lp_ensure_session local text="$1" label="${2:-Password}"
lpass ls --format="%an\t%ag" 2>/dev/null if df_cmd_exists wl-copy; then
echo -n "$text" | wl-copy
elif df_cmd_exists xclip; then
echo -n "$text" | xclip -selection clipboard
else
df_print_error "No clipboard tool (install wl-clipboard or xclip)"
return 1
fi
df_print_success "$label copied (clears in ${PW_CLIP_TIME}s)"
(sleep "$PW_CLIP_TIME" && { wl-copy "" 2>/dev/null || xclip -selection clipboard < /dev/null 2>/dev/null; }) &
} }
_lp_get() {
local item="$1"
local field="${2:-password}"
_lp_ensure_session
case "$field" in
password) lpass show --password "$item" 2>/dev/null ;;
username) lpass show --username "$item" 2>/dev/null ;;
url) lpass show --url "$item" 2>/dev/null ;;
notes) lpass show --notes "$item" 2>/dev/null ;;
*) lpass show --field="$field" "$item" 2>/dev/null ;;
esac
}
_lp_otp() {
local item="$1"
_lp_ensure_session
lpass show --otp "$item" 2>/dev/null
}
_lp_search() {
local query="$1"
_lp_ensure_session
lpass ls 2>/dev/null | grep -i "$query"
}
# ============================================================================
# Unified Interface
# ============================================================================
pw() { pw() {
local cmd="${1:-help}" local cmd="${1:-search}"
shift
if ! command -v lpass &>/dev/null; then
echo -e "${DF_RED}${DF_NC} LastPass CLI (lpass) not installed"
echo "Install with: yay -S lastpass-cli"
return 1
fi
case "$cmd" in case "$cmd" in
list|ls|l) login) lpass login --trust "${LPASS_USER:-}" ;;
df_print_func_name "LastPass Vault" logout) lpass logout -f; df_print_success "Logged out" ;;
_lp_list sync) _pw_check || return 1; df_print_step "Syncing..."; lpass sync; df_print_success "Synced" ;;
show) _pw_check || return 1; [[ -z "$2" ]] && { echo "Usage: pw show <entry>"; return 1; }; lpass show "$2" ;;
gen|generate)
local len="${2:-20}"
local pass=$(tr -dc 'A-Za-z0-9!@#$%^&*' < /dev/urandom | head -c "$len")
_pw_copy "$pass" "Generated password"
;; ;;
list|ls) _pw_check || return 1; df_print_func_name "Password Entries"; lpass ls --long ;;
get|g|show) search|*)
local item="$1" _pw_check || return 1
local field="${2:-password}" local query="$1"; [[ "$cmd" == "search" ]] && query="$2"
[[ -z "$item" ]] && { echo "Usage: pw get <item> [field]"; return 1; } if df_cmd_exists fzf && [[ -z "$query" ]]; then
_lp_get "$item" "$field" local entry=$(lpass ls --format "%an (%au) [%ai]" 2>/dev/null | fzf $(df_fzf_opts) --prompt='Password > ')
;; [[ -z "$entry" ]] && return
local id=$(echo "$entry" | grep -oP '\[\K[^\]]+(?=\]$)')
otp|totp|2fa) local pass=$(lpass show --password "$id" 2>/dev/null)
local item="$1" [[ -n "$pass" ]] && _pw_copy "$pass" || df_print_error "Could not retrieve"
[[ -z "$item" ]] && { echo "Usage: pw otp <item>"; return 1; }
_lp_otp "$item"
;;
search|find|s)
local query="$1"
[[ -z "$query" ]] && { echo "Usage: pw search <query>"; return 1; }
df_print_func_name "LastPass Search: $query"
_lp_search "$query"
;;
copy|cp|c)
local item="$1"
local field="${2:-password}"
[[ -z "$item" ]] && { echo "Usage: pw copy <item> [field]"; return 1; }
local value=$(_lp_get "$item" "$field")
if [[ -n "$value" ]]; then
echo -n "$value" | xclip -selection clipboard 2>/dev/null || \
echo -n "$value" | xsel --clipboard 2>/dev/null || \
{ echo "Could not copy to clipboard"; return 1; }
echo -e "${DF_GREEN}${DF_NC} Copied to clipboard"
else else
echo -e "${DF_RED}${DF_NC} Item not found or empty" [[ -z "$query" ]] && { echo "Usage: pw <search-term>"; return 1; }
return 1 local results=$(lpass ls --format "%an [%ai]" 2>/dev/null | grep -i "$query")
local count=$(echo "$results" | grep -c . 2>/dev/null || echo 0)
if (( count == 0 )); then
df_print_warning "No entries for: $query"
elif (( count == 1 )); then
local id=$(echo "$results" | grep -oP '\[\K[^\]]+(?=\]$)')
_pw_copy "$(lpass show --password "$id" 2>/dev/null)"
else
df_print_warning "Multiple entries:"
echo "$results" | while read -r l; do df_print_indent "$l"; done
fi
fi fi
;; ;;
help|--help|-h)
lock) df_print_func_name "Password Manager"
lpass logout -f 2>/dev/null cat << 'EOF'
echo -e "${DF_GREEN}${DF_NC} Logged out of LastPass" pw <search> Search and copy password
;; pw show <n> Show entry details
pw list List all entries
help|--help|-h|*) pw gen [len] Generate password (default: 20)
df_print_func_name "Password Manager CLI" pw sync Sync vault
echo "Usage: pw <command> [args]" pw login/logout Auth commands
echo EOF
echo "Commands:"
echo " list List all items"
echo " get <item> [field] Get field (default: password)"
echo " otp <item> Get OTP/TOTP code"
echo " search <query> Search items"
echo " copy <item> [field] Copy to clipboard"
echo " lock Logout/lock session"
echo " help Show this help"
echo
echo "Fields: password, username, url, notes, or custom field name"
echo
echo "Examples:"
echo " pw get github"
echo " pw get github username"
echo " pw otp github"
echo " pw copy aws"
echo " pw search mail"
echo
echo "Install: yay -S lastpass-cli"
;; ;;
esac esac
} }
# ============================================================================ alias pwc='pw' pws='pw show' pwg='pw gen' pwl='pw list'
# Aliases
# ============================================================================
alias pwl='pw list'
alias pwg='pw get'
alias pwc='pw copy'
alias pws='pw search'
# ============================================================================
# FZF Integration (if available)
# ============================================================================
if command -v fzf &>/dev/null; then
pwf() {
if ! command -v lpass &>/dev/null; then
echo "LastPass CLI not installed"
return 1
fi
local item=$(_lp_list | fzf --height=40% --reverse | cut -f1)
if [[ -n "$item" ]]; then
pw copy "$item"
fi
}
pwof() {
if ! command -v lpass &>/dev/null; then
echo "LastPass CLI not installed"
return 1
fi
local item=$(_lp_list | fzf --height=40% --reverse | cut -f1)
if [[ -n "$item" ]]; then
local otp=$(pw otp "$item")
if [[ -n "$otp" ]]; then
echo -n "$otp" | xclip -selection clipboard 2>/dev/null || \
echo -n "$otp" | xsel --clipboard 2>/dev/null
echo -e "${DF_GREEN}${DF_NC} OTP copied: $otp"
fi
fi
}
fi

View File

@@ -1,488 +1,173 @@
# ============================================================================ # ============================================================================
# Python Project Template Functions # Python Project Template Functions
# ============================================================================ # ============================================================================
# Quick project scaffolding with virtual environments
#
# Usage:
# py-new <project_name> # Create new Python project
# py-django <project_name> # Create Django project
# py-flask <project_name> # Create Flask project
# py-fastapi <project_name> # Create FastAPI project
# py-data <project_name> # Create data science project
# py-cli <project_name> # Create CLI tool project
# ============================================================================
# Source shared colors (with fallback) source "${0:A:h}/../lib/utils.zsh" 2>/dev/null || \
source "${0:A:h}/../lib/colors.zsh" 2>/dev/null || \ source "$HOME/.dotfiles/zsh/lib/utils.zsh" 2>/dev/null
source "$HOME/.dotfiles/zsh/lib/colors.zsh" 2>/dev/null || {
typeset -g DF_GREEN=$'\033[0;32m' DF_BLUE=$'\033[0;34m'
typeset -g DF_YELLOW=$'\033[1;33m' DF_CYAN=$'\033[0;36m'
typeset -g DF_RED=$'\033[0;31m' DF_NC=$'\033[0m'
}
# ============================================================================ typeset -g PY_PYTHON="${PY_PYTHON:-python3}"
# Configuration typeset -g PY_VENV="${PY_VENV:-venv}"
# ============================================================================ typeset -g PY_GIT_INIT="${PY_GIT_INIT:-true}"
typeset -g PY_TEMPLATE_BASE_DIR="${PY_TEMPLATE_BASE_DIR:-$HOME/projects}" _py_check_name() {
typeset -g PY_TEMPLATE_PYTHON="${PY_TEMPLATE_PYTHON:-python3}" [[ -z "$1" ]] && { df_print_warning "Project name required"; return 1; }
typeset -g PY_TEMPLATE_VENV_NAME="${PY_TEMPLATE_VENV_NAME:-venv}" [[ -d "$1" ]] && { df_print_warning "Directory '$1' exists"; return 1; }
typeset -g PY_TEMPLATE_USE_POETRY="${PY_TEMPLATE_USE_POETRY:-false}"
typeset -g PY_TEMPLATE_GIT_INIT="${PY_TEMPLATE_GIT_INIT:-true}"
# ============================================================================
# Helper Functions
# ============================================================================
_py_print_step() {
echo -e "${DF_BLUE}==>${DF_NC} $1"
}
_py_print_success() {
echo -e "${DF_GREEN}${DF_NC} $1"
}
_py_print_info() {
echo -e "${DF_CYAN}${DF_NC} $1"
}
_py_check_project_name() {
local name="$1"
if [[ -z "$name" ]]; then
echo -e "${DF_YELLOW}${DF_NC} Project name required"
return 1
fi
if [[ -d "$name" ]]; then
echo -e "${DF_YELLOW}${DF_NC} Directory '$name' already exists"
return 1
fi
return 0 return 0
} }
_py_create_venv() { _py_venv() {
local project_dir="$1" df_print_step "Creating virtual environment"
_py_print_step "Creating virtual environment" "$PY_PYTHON" -m venv "$1/$PY_VENV"
df_print_success "Created: $PY_VENV"
if [[ "$PY_TEMPLATE_USE_POETRY" == "true" ]] && command -v poetry &>/dev/null; then
cd "$project_dir"
poetry init --no-interaction
poetry env use "$PY_TEMPLATE_PYTHON"
_py_print_success "Poetry environment created"
else
"$PY_TEMPLATE_PYTHON" -m venv "$project_dir/$PY_TEMPLATE_VENV_NAME"
_py_print_success "Virtual environment created: $PY_TEMPLATE_VENV_NAME"
fi
} }
_py_create_gitignore() { _py_gitignore() {
local project_dir="$1" cat > "$1/.gitignore" << 'EOF'
cat > "$project_dir/.gitignore" << 'EOF'
__pycache__/ __pycache__/
*.py[cod] *.py[cod]
*$py.class
*.so *.so
.Python
build/ build/
dist/ dist/
*.egg-info/ *.egg-info/
venv/ venv/
env/ .venv/
.venv
.vscode/
.idea/
*.swp
.pytest_cache/
.coverage
htmlcov/
.env .env
*.log *.log
*.db .pytest_cache/
*.sqlite3
.mypy_cache/ .mypy_cache/
EOF EOF
_py_print_success "Created .gitignore" df_print_success "Created .gitignore"
} }
_py_init_git() { _py_git() {
local project_dir="$1" [[ "$PY_GIT_INIT" == "true" ]] && { cd "$1"; git init; git add .; git commit -m "Initial commit"; df_print_success "Git initialized"; }
if [[ "$PY_TEMPLATE_GIT_INIT" == "true" ]]; then
cd "$project_dir"
git init
git add .
git commit -m "Initial commit: project scaffolding"
_py_print_success "Git repository initialized"
fi
} }
_py_show_next_steps() { _py_next() {
local project_name="$1" echo ""
local has_venv="$2" df_print_section "Next steps"
echo df_print_indent "cd $1"
echo -e "${DF_CYAN}Next steps:${DF_NC}" df_print_indent "source $PY_VENV/bin/activate"
echo " cd $project_name"
if [[ "$has_venv" == "true" ]]; then
if [[ "$PY_TEMPLATE_USE_POETRY" == "true" ]]; then
echo " poetry shell"
else
echo " source $PY_TEMPLATE_VENV_NAME/bin/activate"
fi
fi
echo " # Start coding!"
echo
} }
# ============================================================================
# Base Python Project Template
# ============================================================================
py-new() { py-new() {
local project_name="$1" _py_check_name "$1" || return 1
_py_check_project_name "$project_name" || return 1 df_print_func_name "Python Project: $1"
mkdir -p "$1"/{src,tests}
df_print_func_name "Python Project: $project_name" touch "$1/src/__init__.py" "$1/tests/__init__.py"
cat > "$1/src/main.py" << 'EOF'
_py_print_step "Creating project structure"
mkdir -p "$project_name"/{src,tests,docs}
touch "$project_name/src/__init__.py"
touch "$project_name/tests/__init__.py"
cat > "$project_name/src/main.py" << 'EOF'
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Main module."""
def main(): def main():
print("Hello from Python!") print("Hello!")
if __name__ == "__main__": if __name__ == "__main__":
main() main()
EOF EOF
echo "# Dependencies" > "$1/requirements.txt"
cat > "$project_name/requirements.txt" << 'EOF' _py_venv "$1"; _py_gitignore "$1"; _py_git "$1"
# Production dependencies df_print_success "Created: $1"
# Development: pytest, black, flake8, mypy _py_next "$1"
EOF
_py_print_success "Project structure created"
_py_create_venv "$project_name"
_py_create_gitignore "$project_name"
_py_init_git "$project_name"
echo
_py_print_success "Project '$project_name' created successfully!"
_py_show_next_steps "$project_name" "true"
} }
# ============================================================================
# Django Project Template
# ============================================================================
py-django() {
local project_name="$1"
_py_check_project_name "$project_name" || return 1
df_print_func_name "Django Project: $project_name"
mkdir -p "$project_name"
_py_create_venv "$project_name"
_py_print_step "Installing Django"
cd "$project_name"
"$PY_TEMPLATE_VENV_NAME/bin/pip" install django
_py_print_step "Creating Django project structure"
"$PY_TEMPLATE_VENV_NAME/bin/django-admin" startproject config .
cat > "requirements.txt" << 'EOF'
Django>=4.2.0
python-decouple>=3.8
EOF
mkdir -p apps static templates media
_py_create_gitignore "."
_py_init_git "."
cd ..
echo
_py_print_success "Django project '$project_name' created!"
_py_show_next_steps "$project_name" "true"
}
# ============================================================================
# Flask Project Template
# ============================================================================
py-flask() { py-flask() {
local project_name="$1" _py_check_name "$1" || return 1
_py_check_project_name "$project_name" || return 1 df_print_func_name "Flask Project: $1"
mkdir -p "$1"/{app/{templates,static},tests}
df_print_func_name "Flask Project: $project_name" _py_venv "$1"
df_print_step "Installing Flask"
mkdir -p "$project_name"/{app/{templates,static/{css,js}},tests} "$1/$PY_VENV/bin/pip" install flask -q
_py_create_venv "$project_name" cat > "$1/app/__init__.py" << 'EOF'
cd "$project_name"
_py_print_step "Installing Flask"
"$PY_TEMPLATE_VENV_NAME/bin/pip" install flask
cat > "app/__init__.py" << 'EOF'
from flask import Flask from flask import Flask
def create_app():
def create_app(config=None):
app = Flask(__name__) app = Flask(__name__)
if config:
app.config.from_object(config)
from app.routes import main from app.routes import main
app.register_blueprint(main) app.register_blueprint(main)
return app return app
EOF EOF
cat > "$1/app/routes.py" << 'EOF'
cat > "app/routes.py" << 'EOF'
from flask import Blueprint, render_template from flask import Blueprint, render_template
main = Blueprint('main', __name__) main = Blueprint('main', __name__)
@main.route('/') @main.route('/')
def index(): def index():
return render_template('index.html') return render_template('index.html')
EOF EOF
echo '<!DOCTYPE html><html><body><h1>Flask</h1></body></html>' > "$1/app/templates/index.html"
cat > "app.py" << 'EOF' cat > "$1/app.py" << 'EOF'
#!/usr/bin/env python3 #!/usr/bin/env python3
from app import create_app from app import create_app
app = create_app() app = create_app()
if __name__ == '__main__': if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port=5000) app.run(debug=True)
EOF EOF
chmod +x app.py echo "Flask>=3.0.0" > "$1/requirements.txt"
_py_gitignore "$1"; _py_git "$1"
cat > "app/templates/index.html" << 'EOF' df_print_success "Created: $1"
<!DOCTYPE html> _py_next "$1"
<html><head><title>Flask App</title></head>
<body><h1>Welcome to Flask!</h1></body></html>
EOF
cat > "requirements.txt" << 'EOF'
Flask>=3.0.0
python-decouple>=3.8
EOF
_py_create_gitignore "."
_py_init_git "."
cd ..
echo
_py_print_success "Flask project '$project_name' created!"
_py_show_next_steps "$project_name" "true"
} }
# ============================================================================
# FastAPI Project Template
# ============================================================================
py-fastapi() { py-fastapi() {
local project_name="$1" _py_check_name "$1" || return 1
_py_check_project_name "$project_name" || return 1 df_print_func_name "FastAPI Project: $1"
mkdir -p "$1"/{app,tests}
df_print_func_name "FastAPI Project: $project_name" _py_venv "$1"
df_print_step "Installing FastAPI"
mkdir -p "$project_name"/{app/{api,models,schemas},tests} "$1/$PY_VENV/bin/pip" install fastapi uvicorn -q
_py_create_venv "$project_name" cat > "$1/app/main.py" << 'EOF'
cd "$project_name"
_py_print_step "Installing FastAPI"
"$PY_TEMPLATE_VENV_NAME/bin/pip" install fastapi uvicorn[standard] pydantic
touch "app/__init__.py"
cat > "app/main.py" << 'EOF'
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware app = FastAPI()
app = FastAPI(title="My API", version="0.1.0")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/") @app.get("/")
def root(): def root():
return {"message": "Welcome to FastAPI"} return {"message": "Hello"}
@app.get("/health") @app.get("/health")
def health(): def health():
return {"status": "healthy"} return {"status": "ok"}
EOF EOF
cat > "$1/run.py" << 'EOF'
cat > "run.py" << 'EOF'
#!/usr/bin/env python3 #!/usr/bin/env python3
import uvicorn import uvicorn
if __name__ == "__main__": if __name__ == "__main__":
uvicorn.run("app.main:app", host="0.0.0.0", port=8000, reload=True) uvicorn.run("app.main:app", host="0.0.0.0", port=8000, reload=True)
EOF EOF
chmod +x run.py echo -e "fastapi>=0.104.0\nuvicorn>=0.24.0" > "$1/requirements.txt"
_py_gitignore "$1"; _py_git "$1"
cat > "requirements.txt" << 'EOF' df_print_success "Created: $1"
fastapi>=0.104.0 df_print_info "Docs: http://localhost:8000/docs"
uvicorn[standard]>=0.24.0 _py_next "$1"
pydantic>=2.5.0
EOF
_py_create_gitignore "."
_py_init_git "."
cd ..
echo
_py_print_success "FastAPI project '$project_name' created!"
_py_print_info "Docs at: http://localhost:8000/docs"
_py_show_next_steps "$project_name" "true"
} }
# ============================================================================
# Data Science Project Template
# ============================================================================
py-data() {
local project_name="$1"
_py_check_project_name "$project_name" || return 1
df_print_func_name "Data Science Project: $project_name"
mkdir -p "$project_name"/{data/{raw,processed},notebooks,src,models,reports/figures}
_py_create_venv "$project_name"
cd "$project_name"
_py_print_step "Installing data science packages"
"$PY_TEMPLATE_VENV_NAME/bin/pip" install pandas numpy matplotlib seaborn jupyter
touch "src/__init__.py"
touch data/raw/.gitkeep data/processed/.gitkeep
cat > "requirements.txt" << 'EOF'
pandas>=2.1.0
numpy>=1.24.0
matplotlib>=3.8.0
seaborn>=0.13.0
jupyter>=1.0.0
scikit-learn>=1.3.0
EOF
_py_create_gitignore "."
cat >> ".gitignore" << 'EOF'
*.pkl
*.h5
*.parquet
data/raw/*
data/processed/*
!data/raw/.gitkeep
!data/processed/.gitkeep
models/*.pkl
.ipynb_checkpoints
EOF
_py_init_git "."
cd ..
echo
_py_print_success "Data science project '$project_name' created!"
_py_print_info "Start Jupyter: jupyter notebook"
_py_show_next_steps "$project_name" "true"
}
# ============================================================================
# CLI Tool Project Template
# ============================================================================
py-cli() { py-cli() {
local project_name="$1" _py_check_name "$1" || return 1
_py_check_project_name "$project_name" || return 1 df_print_func_name "CLI Project: $1"
mkdir -p "$1"/{src/$1,tests}
df_print_func_name "CLI Tool Project: $project_name" _py_venv "$1"
df_print_step "Installing click"
mkdir -p "$project_name"/{src/$project_name,tests} "$1/$PY_VENV/bin/pip" install click -q
_py_create_venv "$project_name" echo '__version__ = "0.1.0"' > "$1/src/$1/__init__.py"
cat > "$1/src/$1/cli.py" << 'EOF'
cd "$project_name"
_py_print_step "Installing click"
"$PY_TEMPLATE_VENV_NAME/bin/pip" install click
cat > "src/$project_name/__init__.py" << 'EOF'
__version__ = "0.1.0"
EOF
cat > "src/$project_name/cli.py" << 'EOF'
#!/usr/bin/env python3 #!/usr/bin/env python3
import click import click
@click.group() @click.group()
@click.version_option() @click.version_option()
def cli(): def cli():
"""CLI tool - A command-line utility."""
pass pass
@cli.command() @cli.command()
@click.argument('name', default='World') @click.argument('name', default='World')
def greet(name): def greet(name):
"""Greet someone."""
click.echo(f"Hello, {name}!") click.echo(f"Hello, {name}!")
if __name__ == '__main__': if __name__ == '__main__':
cli() cli()
EOF EOF
echo "click>=8.1.0" > "$1/requirements.txt"
cat > "setup.py" << EOF _py_gitignore "$1"; _py_git "$1"
from setuptools import setup, find_packages df_print_success "Created: $1"
df_print_info "Install: pip install -e $1"
setup( _py_next "$1"
name="$project_name",
version="0.1.0",
packages=find_packages(where="src"),
package_dir={"": "src"},
install_requires=["click>=8.0.0"],
entry_points={"console_scripts": ["$project_name=$project_name.cli:cli"]},
)
EOF
cat > "requirements.txt" << 'EOF'
click>=8.1.0
EOF
_py_create_gitignore "."
_py_init_git "."
cd ..
echo
_py_print_success "CLI tool project '$project_name' created!"
_py_print_info "Install with: pip install -e $project_name"
_py_show_next_steps "$project_name" "true"
} }
# ============================================================================
# Aliases
# ============================================================================
alias pynew='py-new'
alias pydjango='py-django'
alias pyflask='py-flask'
alias pyfast='py-fastapi'
alias pydata='py-data'
alias pycli='py-cli'
# Quick venv activation
venv() { venv() {
if [[ -d "venv" ]]; then [[ -d "venv" ]] && source venv/bin/activate && return
source venv/bin/activate [[ -d ".venv" ]] && source .venv/bin/activate && return
elif [[ -d ".venv" ]]; then df_print_error "No venv found"
source .venv/bin/activate
elif [[ -d "env" ]]; then
source env/bin/activate
else
echo "No virtual environment found (venv, .venv, or env)"
return 1
fi
} }
alias pynew='py-new' pyflask='py-flask' pyfast='py-fastapi' pycli='py-cli'

View File

@@ -1,291 +1,82 @@
# ============================================================================ # ============================================================================
# Smart Command Suggestions for Zsh # Smart Command Suggestions for Zsh
# ============================================================================ # ============================================================================
# Provides intelligent suggestions when commands fail or could be improved
#
# Features:
# - Typo correction for common commands
# - Suggests existing aliases for frequently typed commands
# - "Did you mean?" for unknown commands
# - Package installation suggestions for missing commands
# ============================================================================
# Source shared colors (with fallback) source "${0:A:h}/../lib/utils.zsh" 2>/dev/null || \
source "${0:A:h}/../lib/colors.zsh" 2>/dev/null || \ source "$HOME/.dotfiles/zsh/lib/utils.zsh" 2>/dev/null
source "$HOME/.dotfiles/zsh/lib/colors.zsh" 2>/dev/null || {
typeset -g DF_CYAN=$'\033[0;36m' DF_YELLOW=$'\033[1;33m'
typeset -g DF_GREEN=$'\033[0;32m' DF_RED=$'\033[0;31m'
typeset -g DF_DIM=$'\033[2m' DF_NC=$'\033[0m'
}
# ============================================================================
# Configuration
# ============================================================================
typeset -g SMART_SUGGEST_ENABLED=true typeset -g SMART_SUGGEST_ENABLED=true
typeset -g SMART_SUGGEST_TYPOS=true
typeset -g SMART_SUGGEST_ALIASES=true
typeset -g SMART_SUGGEST_PACKAGES=true
typeset -g SMART_SUGGEST_HISTORY=true
typeset -g SMART_SUGGEST_TRACK_FILE="$HOME/.cache/smart-suggest-track" typeset -g SMART_SUGGEST_TRACK_FILE="$HOME/.cache/smart-suggest-track"
# ============================================================================
# Common Typo Database
# ============================================================================
typeset -gA TYPO_CORRECTIONS=( typeset -gA TYPO_CORRECTIONS=(
# Git typos [gti]="git" [gitt]="git" [got]="git" [gi]="git"
[gti]="git" [gitt]="git" [got]="git" [gut]="git" [gi]="git" [gitst]="git st" [gits]="git s" [gitp]="git p"
[giit]="git" [ggit]="git" [gitst]="git st" [gits]="git s" [psuh]="push" [psull]="pull" [pul]="pull"
[gitl]="git l" [gitd]="git d" [gitp]="git p" [stauts]="status" [comit]="commit" [commti]="commit"
[psuh]="push" [psull]="pull" [pul]="pull" [puhs]="push" [chekcout]="checkout" [branhc]="branch" [marge]="merge"
[stauts]="status" [statis]="status" [statuus]="status" [dokcer]="docker" [doker]="docker" [dcoker]="docker"
[comit]="commit" [commti]="commit" [commt]="commit" [sl]="ls" [sls]="ls" [cta]="cat" [grpe]="grep" [gerp]="grep"
[chekcout]="checkout" [chekout]="checkout" [checkou]="checkout" [mkdri]="mkdir" [chmdo]="chmod" [suod]="sudo" [sduo]="sudo"
[branhc]="branch" [barnch]="branch" [bracnh]="branch" [pytohn]="python" [pyhton]="python" [ndoe]="node"
[marge]="merge" [merg]="merge" [stsh]="stash" [stahs]="stash" [vmi]="vim" [cde]="code" [clera]="clear" [exti]="exit"
# Docker typos
[dokcer]="docker" [doker]="docker" [docekr]="docker"
[dcoker]="docker" [dockr]="docker" [docke]="docker"
[docker-compoes]="docker-compose" [docker-compsoe]="docker-compose"
[dokcer-compose]="docker-compose"
# Common command typos
[sl]="ls" [l]="ls" [sls]="ls" [lss]="ls"
[cta]="cat" [catt]="cat" [caat]="cat"
[grpe]="grep" [gerp]="grep" [gre]="grep" [grepp]="grep"
[mkdri]="mkdir" [mkdr]="mkdir" [mdkir]="mkdir" [mdir]="mkdir"
[rn]="rm" [rmm]="rm" [chmdo]="chmod" [chomd]="chmod"
[chonw]="chown" [cown]="chown" [tarr]="tar" [tart]="tar"
[wegt]="wget" [wgte]="wget" [weget]="wget"
[crul]="curl" [curll]="curl"
[pytohn]="python" [pyhton]="python" [pythn]="python"
[pyton]="python" [pthon]="python" [pytho]="python"
[ndoe]="node" [noed]="node" [noode]="node"
[npn]="npm" [nmpm]="npm" [nppm]="npm"
[yran]="yarn" [yaarn]="yarn" [yanr]="yarn"
[suod]="sudo" [sudi]="sudo" [sduo]="sudo" [sudoo]="sudo"
[sssh]="ssh" [shh]="ssh" [scpp]="scp" [spcp]="scp"
[vmi]="vim" [imv]="vim" [viim]="vim"
[cde]="code" [cdoe]="code" [cod]="code"
[clera]="clear" [cler]="clear" [claer]="clear"
[ecoh]="echo" [ehco]="echo" [echoo]="echo"
[exti]="exit" [ext]="exit" [exitt]="exit" [eixt]="exit"
[histroy]="history" [hisotry]="history" [hsitory]="history"
[histrory]="history" [maek]="make" [mkae]="make"
[amke]="make" [makee]="make" [ccd]="cd" [cdd]="cd"
) )
# ============================================================================
# Package Manager Detection
# ============================================================================
_ss_get_package_manager() {
if command -v apt-get &>/dev/null; then echo "apt"
elif command -v dnf &>/dev/null; then echo "dnf"
elif command -v pacman &>/dev/null; then echo "pacman"
elif command -v brew &>/dev/null; then echo "brew"
else echo ""
fi
}
typeset -gA COMMAND_PACKAGES=( typeset -gA COMMAND_PACKAGES=(
[htop]="htop" [tree]="tree" [jq]="jq" [htop]="htop" [tree]="tree" [jq]="jq" [fd]="fd" [rg]="ripgrep"
[fd]="fd-find:apt fd:pacman fd:brew" [rg]="ripgrep" [bat]="bat" [eza]="eza" [fzf]="fzf" [tldr]="tldr" [ncdu]="ncdu"
[bat]="bat" [eza]="eza" [exa]="exa" [fzf]="fzf" [lazygit]="lazygit" [neofetch]="neofetch" [delta]="git-delta"
[tldr]="tldr" [ncdu]="ncdu" [duf]="duf" [dust]="dust"
[procs]="procs" [bottom]="bottom" [btm]="bottom"
[lazygit]="lazygit" [lazydocker]="lazydocker"
[neofetch]="neofetch" [fastfetch]="fastfetch"
[httpie]="httpie" [http]="httpie"
[delta]="git-delta:apt delta:pacman git-delta:brew"
[glow]="glow" [navi]="navi"
) )
_ss_suggest_package() { _ss_track() {
local cmd="$1"
local pm=$(_ss_get_package_manager)
[[ -z "$pm" ]] && return 1
local pkg_info="${COMMAND_PACKAGES[$cmd]}"
[[ -z "$pkg_info" ]] && return 1
local pkg=""
if [[ "$pkg_info" == *":"* ]]; then
for entry in ${(s: :)pkg_info}; do
local p="${entry%%:*}"
local m="${entry##*:}"
if [[ "$m" == "$pm" ]]; then
pkg="$p"
break
fi
done
[[ -z "$pkg" ]] && pkg="${${(s: :)pkg_info}[1]%%:*}"
else
pkg="$pkg_info"
fi
[[ -z "$pkg" ]] && return 1
local install_cmd=""
case "$pm" in
apt) install_cmd="sudo apt install $pkg" ;;
dnf) install_cmd="sudo dnf install $pkg" ;;
pacman) install_cmd="sudo pacman -S $pkg" ;;
brew) install_cmd="brew install $pkg" ;;
esac
echo "$install_cmd"
}
# ============================================================================
# Alias Tracking
# ============================================================================
_ss_track_command() {
[[ "$SMART_SUGGEST_ALIASES" != true ]] && return
local cmd="$1" local cmd="$1"
[[ ${#cmd} -lt 8 ]] && return [[ ${#cmd} -lt 8 ]] && return
df_ensure_dir "$(dirname "$SMART_SUGGEST_TRACK_FILE")"
mkdir -p "$(dirname "$SMART_SUGGEST_TRACK_FILE")"
echo "$cmd" >> "$SMART_SUGGEST_TRACK_FILE" echo "$cmd" >> "$SMART_SUGGEST_TRACK_FILE"
local count=$(grep -Fc "$cmd" "$SMART_SUGGEST_TRACK_FILE" 2>/dev/null || echo 0) local count=$(grep -Fc "$cmd" "$SMART_SUGGEST_TRACK_FILE" 2>/dev/null || echo 0)
if (( count >= 10 && count % 10 == 0 )); then
if [[ $count -ge 10 && $((count % 10)) -eq 0 ]]; then
_ss_suggest_alias_for "$cmd" "$count"
fi
}
_ss_suggest_alias_for() {
local cmd="$1"
local count="$2"
local existing=$(alias | grep -F "='$cmd'" | head -1 | cut -d= -f1) local existing=$(alias | grep -F "='$cmd'" | head -1 | cut -d= -f1)
[[ -n "$existing" ]] && df_print_info "You have alias: $existing" || \
if [[ -n "$existing" ]]; then df_print_info "Consider: alias xyz='$cmd'"
echo
echo -e "${DF_CYAN}💡 Tip:${DF_NC} You've typed '${DF_YELLOW}$cmd${DF_NC}' $count times"
echo -e " You already have an alias: ${DF_GREEN}$existing${DF_NC}"
else
local suggested=$(echo "$cmd" | awk '{
for(i=1; i<=NF && i<=3; i++)
printf substr($i,1,1)
}')
echo
echo -e "${DF_CYAN}💡 Tip:${DF_NC} You've typed '${DF_YELLOW}$cmd${DF_NC}' $count times"
echo -e " Consider adding: ${DF_GREEN}alias $suggested='$cmd'${DF_NC}"
fi fi
} }
# ============================================================================
# Command Not Found Handler
# ============================================================================
command_not_found_handler() { command_not_found_handler() {
local cmd="$1" local cmd="$1"; shift
shift [[ "$SMART_SUGGEST_ENABLED" != true ]] && { echo "zsh: command not found: $cmd"; return 127; }
local args="$@"
[[ "$SMART_SUGGEST_ENABLED" != true ]] && { df_print_error "Command not found: $cmd"
echo "zsh: command not found: $cmd"
return 127
}
echo -e "${DF_RED}${DF_NC} Command not found: ${DF_YELLOW}$cmd${DF_NC}"
local suggestion_made=false
if [[ "$SMART_SUGGEST_TYPOS" == true ]]; then
local correction="${TYPO_CORRECTIONS[$cmd]}" local correction="${TYPO_CORRECTIONS[$cmd]}"
if [[ -n "$correction" ]]; then [[ -n "$correction" ]] && { df_print_info "Did you mean: $correction?"; df_print_indent "Run: $correction $@"; }
echo -e "${DF_CYAN}${DF_NC} Did you mean: ${DF_GREEN}$correction${DF_NC}?"
echo -e " ${DF_DIM}Run: $correction $args${DF_NC}"
suggestion_made=true
fi
fi
if [[ "$suggestion_made" != true ]]; then local pkg="${COMMAND_PACKAGES[$cmd]}"
local similar=$(compgen -c 2>/dev/null | grep -i "^${cmd:0:3}" | head -3 | tr '\n' ', ' | sed 's/,$//') [[ -n "$pkg" ]] && df_print_info "Install: sudo pacman -S $pkg"
if [[ -n "$similar" ]]; then
echo -e "${DF_CYAN}${DF_NC} Similar commands: ${DF_GREEN}$similar${DF_NC}"
suggestion_made=true
fi
fi
if [[ "$SMART_SUGGEST_PACKAGES" == true ]]; then
local install_cmd=$(_ss_suggest_package "$cmd")
if [[ -n "$install_cmd" ]]; then
echo -e "${DF_CYAN}${DF_NC} To install: ${DF_GREEN}$install_cmd${DF_NC}"
suggestion_made=true
fi
fi
return 127 return 127
} }
# ============================================================================
# Hooks
# ============================================================================
_ss_preexec_hook() {
local cmd="$1"
local first_word="${cmd%% *}"
_ss_track_command "$cmd"
}
_ss_precmd_hook() {
local exit_code=$?
[[ $exit_code -eq 0 ]] && return
local last_cmd=$(fc -ln -1 2>/dev/null | sed 's/^[[:space:]]*//')
[[ -z "$last_cmd" ]] && return
local first_word="${last_cmd%% *}"
if [[ "$first_word" == "git" && $exit_code -ne 0 ]]; then
local git_subcmd=$(echo "$last_cmd" | awk '{print $2}')
local correction="${TYPO_CORRECTIONS[$git_subcmd]}"
if [[ -n "$correction" ]]; then
echo -e "${DF_CYAN}${DF_NC} Did you mean: ${DF_GREEN}git $correction${DF_NC}?"
fi
fi
}
# ============================================================================
# Quick Fix Function
# ============================================================================
fuck() { fuck() {
local last_cmd=$(fc -ln -1 2>/dev/null | sed 's/^[[:space:]]*//') local last=$(fc -ln -1 2>/dev/null | sed 's/^[[:space:]]*//')
local first_word="${last_cmd%% *}" local first="${last%% *}"
local fix="${TYPO_CORRECTIONS[$first]}"
local correction="${TYPO_CORRECTIONS[$first_word]}" [[ -n "$fix" ]] && { df_print_step "Running: ${last/$first/$fix}"; eval "${last/$first/$fix}"; } || df_print_warning "No fix for: $last"
if [[ -n "$correction" ]]; then
local fixed_cmd="${last_cmd/$first_word/$correction}"
echo -e "${DF_GREEN}Running:${DF_NC} $fixed_cmd"
eval "$fixed_cmd"
else
echo "No automatic fix available"
echo "Last command: $last_cmd"
fi
} }
# ============================================================================ _ss_preexec() { _ss_track "$1"; }
# Setup Hooks _ss_precmd() {
# ============================================================================ local exit=$?; (( exit == 0 )) && return
local last=$(fc -ln -1 2>/dev/null | sed 's/^[[:space:]]*//')
[[ "${last%% *}" == "git" ]] && {
local sub=$(echo "$last" | awk '{print $2}')
local fix="${TYPO_CORRECTIONS[$sub]}"
[[ -n "$fix" ]] && df_print_info "Did you mean: git $fix?"
}
}
_ss_setup() { _ss_setup() {
autoload -Uz add-zsh-hook autoload -Uz add-zsh-hook
add-zsh-hook preexec _ss_preexec_hook add-zsh-hook preexec _ss_preexec
add-zsh-hook precmd _ss_precmd_hook add-zsh-hook precmd _ss_precmd
} }
[[ "$SMART_SUGGEST_ENABLED" == true ]] && _ss_setup [[ "$SMART_SUGGEST_ENABLED" == true ]] && _ss_setup

View File

@@ -2,253 +2,97 @@
# Snapper Snapshot Functions for CachyOS/Arch with limine-snapper-sync # Snapper Snapshot Functions for CachyOS/Arch with limine-snapper-sync
# ============================================================================ # ============================================================================
# Source shared colors (with fallback) source "${0:A:h}/../lib/utils.zsh" 2>/dev/null || \
source "${0:A:h}/../lib/colors.zsh" 2>/dev/null || \ source "$HOME/.dotfiles/zsh/lib/utils.zsh" 2>/dev/null
source "$HOME/.dotfiles/zsh/lib/colors.zsh" 2>/dev/null || {
typeset -g DF_GREEN=$'\033[0;32m' DF_YELLOW=$'\033[1;33m'
typeset -g DF_RED=$'\033[0;31m' DF_BLUE=$'\033[0;34m' DF_NC=$'\033[0m'
}
# ============================================================================
# Main Snapshot Function with Limine Validation
# ============================================================================
snap-create() { snap-create() {
local description="$*" local desc="$*"
local snap_config="root" local limine="/boot/limine.conf"
local limine_conf="/boot/limine.conf"
df_print_func_name "Snapper Snapshot Creation" df_print_func_name "Snapper Snapshot Creation"
if [[ -z "$description" ]]; then if [[ -z "$desc" ]]; then
echo -e "${DF_YELLOW}${DF_NC} No description provided" df_print_warning "No description"
echo -n "Enter snapshot description: " echo -n "Description: "; read desc
read description [[ -z "$desc" ]] && { df_print_error "Required"; return 1; }
[[ -z "$description" ]] && { echo -e "${DF_RED}${DF_NC} Description required. Aborting."; return 1; }
fi fi
[[ ! -f "$limine_conf" ]] && { echo -e "${DF_RED}${DF_NC} Limine config not found: $limine_conf"; return 1; } [[ ! -f "$limine" ]] && { df_print_error "Limine not found: $limine"; return 1; }
echo -e "${DF_BLUE}==>${DF_NC} Checking limine.conf state before snapshot" df_print_step "Checking limine.conf before snapshot"
local before_checksum=$(sudo md5sum "$limine_conf" | awk '{print $1}') local before=$(sudo grep -cP "^\\s*///\\d+\\s*│" "$limine" || echo "0")
local before_entries=$(sudo grep -cP "^\\s*///\\d+\\s*│" "$limine_conf" || echo "0") df_print_success "Before: $before entries"
echo -e "${DF_GREEN}${DF_NC} Before: $before_entries snapshot entries" df_print_step "Creating snapshot: \"$desc\""
echo -e "${DF_GREEN}${DF_NC} Before checksum: $before_checksum" local num=$(sudo snapper -c root create --description "$desc" --print-number)
[[ -z "$num" ]] && { df_print_error "Failed"; return 1; }
df_print_success "Created: #$num"
echo -e "\n${DF_BLUE}==>${DF_NC} Creating snapshot: \"$description\"" df_print_step "Triggering limine-snapper-sync..."
sudo systemctl start limine-snapper-sync.service && df_print_success "Triggered" || df_print_warning "May run automatically"
local snapshot_num=$(sudo snapper -c "$snap_config" create --description "$description" --print-number)
[[ -z "$snapshot_num" ]] && { echo -e "${DF_RED}${DF_NC} Failed to create snapshot"; return 1; }
echo -e "${DF_GREEN}${DF_NC} Snapshot created: #$snapshot_num"
echo -e "\n${DF_BLUE}==>${DF_NC} Triggering limine-snapper-sync service..."
if sudo systemctl start limine-snapper-sync.service; then
echo -e "${DF_GREEN}${DF_NC} Service triggered successfully"
else
echo -e "${DF_YELLOW}${DF_NC} Failed to trigger service (may run automatically)"
fi
echo -e "${DF_BLUE}==>${DF_NC} Waiting for limine-snapper-sync to update limine.conf..."
sleep 2 sleep 2
echo -e "${DF_BLUE}==>${DF_NC} Validating limine.conf update" df_print_step "Validating"
local after_checksum=$(sudo md5sum "$limine_conf" | awk '{print $1}') local after=$(sudo grep -cP "^\\s*///\\d+\\s*│" "$limine" || echo "0")
local after_entries=$(sudo grep -cP "^\\s*///\\d+\\s*│" "$limine_conf" || echo "0")
local validation_passed=true if sudo grep -qP "^\\s*///$num\\s*│" "$limine"; then
df_print_success "Snapshot #$num in limine.conf"
if [[ "$before_checksum" == "$after_checksum" ]]; then (( after > before )) && df_print_success "Added $((after - before)) entry"
echo -e "${DF_RED}${DF_NC} limine.conf was NOT updated (checksum unchanged)"
validation_passed=false
else else
echo -e "${DF_GREEN}${DF_NC} limine.conf was updated" df_print_error "Snapshot #$num NOT in limine.conf"
fi return 1
if [[ "$after_entries" -le "$before_entries" ]]; then
echo -e "${DF_RED}${DF_NC} No new snapshot entry added to limine.conf"
validation_passed=false
else
local new_entries=$((after_entries - before_entries))
echo -e "${DF_GREEN}${DF_NC} Added $new_entries new snapshot entry/entries"
fi
echo -e "\n${DF_BLUE}==>${DF_NC} Searching for snapshot #$snapshot_num in limine.conf"
if sudo grep -qP "^\\s*///$snapshot_num\\s*│" "$limine_conf"; then
echo -e "${DF_GREEN}${DF_NC} Found snapshot #$snapshot_num in limine.conf"
echo -e "\n${DF_BLUE}Snapshot entry:${DF_NC}"
local entry_line=$(sudo grep -nP "^\\s*///$snapshot_num\\s*│" "$limine_conf" | head -n 1 | cut -d: -f1)
[[ -n "$entry_line" ]] && sudo sed -n "${entry_line}p; $((entry_line+1))p" "$limine_conf" | sed 's/^/ /'
else
echo -e "${DF_RED}${DF_NC} Snapshot #$snapshot_num NOT found in limine.conf"
validation_passed=false
fi fi
echo "" echo ""
echo -e "${DF_CYAN}Summary:${DF_NC}" df_print_section "Summary"
echo -e " Snapshot Number: #$snapshot_num" df_print_indent "Number: #$num"
echo -e " Description: \"$description\"" df_print_indent "Description: $desc"
echo -e " Config: $snap_config"
echo -e " Before entries: $before_entries"
echo -e " After entries: $after_entries"
if [[ "$validation_passed" == true ]]; then
echo -e " Status: ${DF_GREEN}✓ VALIDATED${DF_NC}"
echo -e "\n${DF_GREEN}${DF_NC} Snapshot created and limine.conf successfully updated!"
return 0
else
echo -e " Status: ${DF_RED}✗ VALIDATION FAILED${DF_NC}"
echo -e "\n${DF_RED}${DF_NC} Snapshot created but limine.conf validation failed!"
echo -e "${DF_YELLOW}${DF_NC} Check if limine-snapper-sync service is running properly"
echo -e "${DF_YELLOW}Run:${DF_NC} sudo systemctl status limine-snapper-sync.service"
return 1
fi
} }
# ============================================================================
# Helper Functions
# ============================================================================
snap-list() { snap-list() {
local count="${1:-10}" local count="${1:-10}"
df_print_func_name "Snapper Snapshots (last $count)" df_print_func_name "Snapper Snapshots (last $count)"
sudo snapper -c root list | tail -n "$((count + 1))" sudo snapper -c root list | tail -n "$((count + 1))"
} }
snap-show() { snap-show() {
[[ -z "$1" ]] && { echo -e "${DF_RED}${DF_NC} Usage: snap-show <snapshot_number>"; return 1; } [[ -z "$1" ]] && { echo "Usage: snap-show <num>"; return 1; }
df_print_func_name "Snapshot #$1"
df_print_func_name "Snapshot #$1 Details"
sudo snapper -c root list | grep "^\s*$1\s" sudo snapper -c root list | grep "^\s*$1\s"
echo ""
echo -e "\n${DF_CYAN}In limine.conf:${DF_NC}" df_print_section "In limine.conf"
if sudo grep -qP "^\\s*///$1\\s*│" /boot/limine.conf; then sudo grep -qP "^\\s*///$1\\s*│" /boot/limine.conf && \
local entry_line=$(sudo grep -nP "^\\s*///$1\\s*│" /boot/limine.conf | head -n 1 | cut -d: -f1) sudo grep -P "^\\s*///$1\\s*│" /boot/limine.conf || df_print_warning "Not found"
[[ -n "$entry_line" ]] && sudo sed -n "${entry_line}p; $((entry_line+1))p" /boot/limine.conf
else
echo -e "${DF_YELLOW}${DF_NC} Not found in limine.conf"
fi
} }
snap-delete() { snap-delete() {
[[ -z "$1" ]] && { echo -e "${DF_RED}${DF_NC} Usage: snap-delete <snapshot_number>"; return 1; } [[ -z "$1" ]] && { echo "Usage: snap-delete <num>"; return 1; }
df_print_func_name "Delete Snapshot #$1"
local snapshot_num="$1" local before=$(sudo grep -cP "^\\s*///\\d+\\s*│" /boot/limine.conf || echo "0")
local limine_conf="/boot/limine.conf" sudo snapper -c root delete "$1" && df_print_success "Deleted #$1" || { df_print_error "Failed"; return 1; }
df_print_func_name "Delete Snapshot #$snapshot_num" df_print_step "Syncing limine..."
sudo systemctl start limine-snapper-sync.service; sleep 2
local before_entries=$(sudo grep -cP "^\\s*///\\d+\\s*│" "$limine_conf" || echo "0") sudo grep -qP "^\\s*///$1\\s*│" /boot/limine.conf && df_print_error "Still in limine!" || df_print_success "Removed from limine"
sudo snapper -c root delete "$snapshot_num"
if [[ $? -eq 0 ]]; then
echo -e "${DF_GREEN}${DF_NC} Snapshot #$snapshot_num deleted"
echo -e "${DF_BLUE}==>${DF_NC} Triggering limine-snapper-sync..."
sudo systemctl start limine-snapper-sync.service
sleep 2
local after_entries=$(sudo grep -cP "^\\s*///\\d+\\s*│" "$limine_conf" || echo "0")
if [[ "$after_entries" -lt "$before_entries" ]]; then
echo -e "${DF_GREEN}${DF_NC} limine.conf updated (removed entry)"
else
echo -e "${DF_YELLOW}${DF_NC} limine.conf may not have been updated"
fi
if ! sudo grep -qP "^\\s*///$snapshot_num\\s*│" "$limine_conf"; then
echo -e "${DF_GREEN}${DF_NC} Snapshot #$snapshot_num removed from limine.conf"
else
echo -e "${DF_RED}${DF_NC} Snapshot #$snapshot_num still in limine.conf!"
fi
else
echo -e "${DF_RED}${DF_NC} Failed to delete snapshot #$snapshot_num"
return 1
fi
}
snap-check-limine() {
local limine_conf="/boot/limine.conf"
df_print_func_name "Limine Snapshot Entries"
[[ ! -f "$limine_conf" ]] && { echo -e "${DF_RED}${DF_NC} Limine config not found: $limine_conf"; return 1; }
local latest_snapshot=$(sudo snapper -c root list | tail -n +3 | grep -v "^\s*0\s" | tail -n 1 | awk '{print $1}')
[[ -z "$latest_snapshot" ]] && { echo -e "${DF_YELLOW}${DF_NC} No snapshots found in snapper"; return 1; }
echo -e "${DF_CYAN}Latest snapshot:${DF_NC} #$latest_snapshot"
echo -e "${DF_BLUE}==>${DF_NC} Checking if latest snapshot is in limine.conf"
if sudo grep -qP "^\\s*///$latest_snapshot\s*│" "$limine_conf"; then
echo -e "${DF_GREEN}${DF_NC} Latest snapshot #$latest_snapshot is present in limine.conf"
else
echo -e "${DF_RED}${DF_NC} Latest snapshot #$latest_snapshot is NOT in limine.conf"
fi
echo -e "\n${DF_BLUE}==>${DF_NC} Counting snapshot entries"
local entry_count=$(sudo grep -cP "^\\s*///\\d+\\s*│" "$limine_conf" || echo "0")
echo -e "${DF_CYAN}Total snapshot entries:${DF_NC} $entry_count"
} }
snap-sync() { snap-sync() {
df_print_func_name "Limine-Snapper-Sync" df_print_func_name "Limine-Snapper-Sync"
df_print_step "Triggering sync..."
echo -e "${DF_BLUE}==>${DF_NC} Manually triggering limine-snapper-sync..." sudo systemctl start limine-snapper-sync.service && { sleep 2; df_print_success "Done"; } || df_print_error "Failed"
if sudo systemctl start limine-snapper-sync.service; then
echo -e "${DF_GREEN}${DF_NC} Service triggered successfully"
sleep 2
echo -e "\n${DF_CYAN}Service status:${DF_NC}"
sudo systemctl status limine-snapper-sync.service --no-pager -l | tail -n 10
else
echo -e "${DF_RED}${DF_NC} Failed to trigger service"
return 1
fi
} }
snap-validate-service() { snap-check() {
df_print_func_name "Limine-Snapper-Sync Service Validation" df_print_func_name "Limine Snapshot Entries"
local latest=$(sudo snapper -c root list | tail -n +3 | grep -v "^\s*0\s" | tail -1 | awk '{print $1}')
echo -e "${DF_BLUE}==>${DF_NC} Checking service unit" [[ -z "$latest" ]] && { df_print_warning "No snapshots"; return 1; }
df_print_info "Latest: #$latest"
if systemctl list-unit-files | grep -q "limine-snapper-sync.service"; then sudo grep -qP "^\\s*///$latest\\s*│" /boot/limine.conf && \
echo -e "${DF_GREEN}${DF_NC} limine-snapper-sync.service unit exists" df_print_success "Latest in limine.conf" || df_print_error "Latest NOT in limine.conf"
else local count=$(sudo grep -cP "^\\s*///\\d+\\s*│" /boot/limine.conf || echo "0")
echo -e "${DF_RED}${DF_NC} limine-snapper-sync.service unit NOT found" df_print_info "Total entries: $count"
echo -e "\n${DF_YELLOW}Install with:${DF_NC} paru -S limine-snapper-sync"
return 1
fi
echo -e "\n${DF_BLUE}==>${DF_NC} Checking if service is enabled"
if systemctl is-enabled limine-snapper-sync.service &>/dev/null; then
echo -e "${DF_GREEN}${DF_NC} Service is enabled"
else
echo -e "${DF_YELLOW}${DF_NC} Service is NOT enabled"
echo -e "${DF_YELLOW}Enable with:${DF_NC} sudo systemctl enable limine-snapper-sync.service"
fi
echo -e "\n${DF_BLUE}==>${DF_NC} Recent service logs (last 10 lines)"
echo ""
sudo journalctl -u limine-snapper-sync.service -n 10 --no-pager | sed 's/^/ /'
echo -e "\n${DF_GREEN}${DF_NC} Validation complete"
} }
# Quick snapshot aliases alias snap='snap-create' snapls='snap-list' snaprm='snap-delete' snapcheck='snap-check'
alias snap='snap-create'
alias snapls='snap-list'
alias snaprm='snap-delete'
alias snapshow='snap-show'
alias snapcheck='snap-check-limine'
alias snapsync='snap-sync'

View File

@@ -1,237 +1,96 @@
# ============================================================================ # ============================================================================
# SSH Session Manager with Tmux Integration # SSH Session Manager with Tmux Integration
# ============================================================================ # ============================================================================
# Manage SSH connections with automatic tmux session handling
# ============================================================================
# Source shared colors (with fallback) source "${0:A:h}/../lib/utils.zsh" 2>/dev/null || \
source "${0:A:h}/../lib/colors.zsh" 2>/dev/null || \ source "$HOME/.dotfiles/zsh/lib/utils.zsh" 2>/dev/null
source "$HOME/.dotfiles/zsh/lib/colors.zsh" 2>/dev/null || {
typeset -g DF_GREEN=$'\033[0;32m' DF_BLUE=$'\033[0;34m'
typeset -g DF_YELLOW=$'\033[1;33m' DF_CYAN=$'\033[0;36m'
typeset -g DF_RED=$'\033[0;31m' DF_NC=$'\033[0m'
}
# ============================================================================
# Configuration
# ============================================================================
typeset -g SSH_PROFILES_FILE="${SSH_PROFILES_FILE:-$HOME/.dotfiles/.ssh-profiles}" typeset -g SSH_PROFILES_FILE="${SSH_PROFILES_FILE:-$HOME/.dotfiles/.ssh-profiles}"
typeset -g SSH_AUTO_TMUX="${SSH_AUTO_TMUX:-true}" typeset -g SSH_AUTO_TMUX="${SSH_AUTO_TMUX:-true}"
typeset -g SSH_TMUX_SESSION_PREFIX="${SSH_TMUX_SESSION_PREFIX:-ssh}" typeset -g SSH_TMUX_PREFIX="${SSH_TMUX_PREFIX:-ssh}"
typeset -g SSH_SYNC_DOTFILES="${SSH_SYNC_DOTFILES:-ask}"
# ============================================================================ _ssh_init() {
# Helper Functions df_ensure_file "$SSH_PROFILES_FILE" "# SSH Profiles: name|user@host|port|key|options|description"
# ============================================================================
_ssh_print_step() { echo -e "${DF_BLUE}==>${DF_NC} $1"; }
_ssh_print_success() { echo -e "${DF_GREEN}${DF_NC} $1"; }
_ssh_print_error() { echo -e "${DF_RED}${DF_NC} $1"; }
_ssh_print_info() { echo -e "${DF_CYAN}${DF_NC} $1"; }
_ssh_init_profiles() {
if [[ ! -f "$SSH_PROFILES_FILE" ]]; then
mkdir -p "$(dirname "$SSH_PROFILES_FILE")"
cat > "$SSH_PROFILES_FILE" << 'EOF'
# SSH Connection Profiles
# Format: name|user@host|port|key_file|options|description
EOF
_ssh_print_success "Created SSH profiles file: $SSH_PROFILES_FILE"
fi
} }
_ssh_parse_profile() { _ssh_parse() {
local name="$1" local line=$(grep "^${1}|" "$SSH_PROFILES_FILE" 2>/dev/null | head -1)
local line=$(grep "^${name}|" "$SSH_PROFILES_FILE" 2>/dev/null | head -1)
[[ -z "$line" ]] && return 1 [[ -z "$line" ]] && return 1
IFS='|' read -r profile_name connection port key_file ssh_opts description <<< "$line" echo "$line" | cut -d'|' -f2-
echo "$connection|$port|$key_file|$ssh_opts|$description"
} }
# ============================================================================
# SSH Profile Management
# ============================================================================
ssh-save() { ssh-save() {
local name="$1" connection="$2" port="${3:-22}" key_file="${4:-}" options="${5:-}" description="${6:-}" local name="$1" conn="$2" port="${3:-22}" key="${4:-}" opts="${5:-}" desc="${6:-}"
[[ -z "$name" || -z "$conn" ]] && { echo "Usage: ssh-save <n> <user@host> [port] [key] [opts] [desc]"; return 1; }
_ssh_init_profiles _ssh_init
grep -q "^${name}|" "$SSH_PROFILES_FILE" 2>/dev/null && {
[[ -z "$name" || -z "$connection" ]] && { df_confirm "Overwrite '$name'?" || return 1
echo "Usage: ssh-save <name> <user@host> [port] [key_file] [options] [description]"
return 1
}
if grep -q "^${name}|" "$SSH_PROFILES_FILE" 2>/dev/null; then
echo -e "${DF_YELLOW}${DF_NC} Profile '$name' already exists"
read -q "REPLY?Overwrite? [y/N]: "; echo
[[ ! "$REPLY" =~ ^[Yy]$ ]] && return 1
grep -v "^${name}|" "$SSH_PROFILES_FILE" > "${SSH_PROFILES_FILE}.tmp" grep -v "^${name}|" "$SSH_PROFILES_FILE" > "${SSH_PROFILES_FILE}.tmp"
mv "${SSH_PROFILES_FILE}.tmp" "$SSH_PROFILES_FILE" mv "${SSH_PROFILES_FILE}.tmp" "$SSH_PROFILES_FILE"
fi }
echo "${name}|${conn}|${port}|${key}|${opts}|${desc}" >> "$SSH_PROFILES_FILE"
echo "${name}|${connection}|${port}|${key_file}|${options}|${description}" >> "$SSH_PROFILES_FILE" df_print_success "Saved: $name$conn"
_ssh_print_success "Saved SSH profile: $name"
echo " Connection: $connection"
[[ "$port" != "22" ]] && echo " Port: $port"
[[ -n "$key_file" ]] && echo " Key: $key_file"
} }
ssh-list() { ssh-list() {
_ssh_init_profiles _ssh_init
df_print_func_name "SSH Profiles"
df_print_func_name "SSH Connection Profiles" local found=false
while IFS='|' read -r name conn port key opts desc; do
local has_profiles=false [[ "$name" =~ ^# || -z "$name" ]] && continue
while IFS='|' read -r name connection port key options description; do found=true
[[ "$name" =~ ^# ]] && continue df_print_indent "$name$conn"
[[ -z "$name" ]] && continue [[ "$port" != "22" && -n "$port" ]] && df_print_indent " Port: $port"
has_profiles=true [[ -n "$desc" ]] && df_print_indent " $desc"
echo -e "${DF_GREEN}${DF_NC} ${DF_CYAN}$name${DF_NC}"
echo " Connection: $connection"
[[ "$port" != "22" && -n "$port" ]] && echo " Port: $port"
[[ -n "$key" ]] && echo " Key: $key"
[[ -n "$description" ]] && echo " Description: $description"
echo
done < "$SSH_PROFILES_FILE" done < "$SSH_PROFILES_FILE"
[[ "$found" != true ]] && df_print_info "No profiles. Use: ssh-save name user@host"
[[ "$has_profiles" != true ]] && {
_ssh_print_info "No profiles saved yet"
echo "Create a profile with: ssh-save myserver user@example.com"
}
} }
ssh-delete() { ssh-delete() {
local name="$1" [[ -z "$1" ]] && { echo "Usage: ssh-delete <n>"; return 1; }
[[ -z "$name" ]] && { echo "Usage: ssh-delete <name>"; return 1; } grep -q "^${1}|" "$SSH_PROFILES_FILE" 2>/dev/null || { df_print_error "Not found: $1"; return 1; }
grep -v "^${1}|" "$SSH_PROFILES_FILE" > "${SSH_PROFILES_FILE}.tmp"
_ssh_init_profiles
if ! grep -q "^${name}|" "$SSH_PROFILES_FILE" 2>/dev/null; then
_ssh_print_error "Profile '$name' not found"
return 1
fi
grep -v "^${name}|" "$SSH_PROFILES_FILE" > "${SSH_PROFILES_FILE}.tmp"
mv "${SSH_PROFILES_FILE}.tmp" "$SSH_PROFILES_FILE" mv "${SSH_PROFILES_FILE}.tmp" "$SSH_PROFILES_FILE"
_ssh_print_success "Deleted profile: $name" df_print_success "Deleted: $1"
} }
ssh-connect() { ssh-connect() {
local name="$1" local name="$1" session="${2:-${SSH_TMUX_PREFIX}-${1}}"
local session_name="${2:-${SSH_TMUX_SESSION_PREFIX}-${name}}" [[ -z "$name" ]] && { ssh-list; return 1; }
_ssh_init
local data=$(_ssh_parse "$name")
[[ -z "$data" ]] && { df_print_error "Not found: $name"; return 1; }
[[ -z "$name" ]] && { echo "Usage: ssh-connect <profile_name>"; ssh-list; return 1; } IFS='|' read -r conn port key opts desc <<< "$data"
df_print_step "Connecting: $name"
[[ -n "$desc" ]] && df_print_indent "$desc"
_ssh_init_profiles local cmd="ssh"
[[ -n "$port" && "$port" != "22" ]] && cmd="$cmd -p $port"
local profile_data=$(_ssh_parse_profile "$name") [[ -n "$key" ]] && cmd="$cmd -i $key"
[[ -z "$profile_data" ]] && { _ssh_print_error "Profile '$name' not found"; return 1; } [[ -n "$opts" ]] && cmd="$cmd $opts"
cmd="$cmd $conn"
IFS='|' read -r connection port key_file ssh_opts description <<< "$profile_data"
_ssh_print_step "Connecting to: $name"
[[ -n "$description" ]] && echo " $description"
local ssh_cmd="ssh"
[[ -n "$port" && "$port" != "22" ]] && ssh_cmd="$ssh_cmd -p $port"
[[ -n "$key_file" ]] && ssh_cmd="$ssh_cmd -i $key_file"
[[ -n "$ssh_opts" ]] && ssh_cmd="$ssh_cmd $ssh_opts"
ssh_cmd="$ssh_cmd $connection"
if [[ "$SSH_AUTO_TMUX" == "true" ]]; then if [[ "$SSH_AUTO_TMUX" == "true" ]]; then
_ssh_print_info "Attaching to tmux session: $session_name" df_print_info "Tmux session: $session"
local tmux_cmd="tmux attach-session -t $session_name 2>/dev/null || tmux new-session -s $session_name" eval "$cmd -t 'tmux attach -t $session 2>/dev/null || tmux new -s $session'"
eval "$ssh_cmd -t '$tmux_cmd'"
else else
eval "$ssh_cmd" eval "$cmd"
fi fi
} }
sshf() { sshf() {
if ! command -v fzf &>/dev/null; then df_require_cmd fzf || return 1
_ssh_print_error "fzf not installed" _ssh_init
return 1
fi
_ssh_init_profiles
local profiles=() local profiles=()
while IFS='|' read -r name connection port key options description; do while IFS='|' read -r name conn port key opts desc; do
[[ "$name" =~ ^# ]] && continue [[ "$name" =~ ^# || -z "$name" ]] && continue
[[ -z "$name" ]] && continue profiles+=("$name|$name$conn")
local display="$name$connection"
[[ -n "$description" ]] && display="$display ($description)"
profiles+=("$name|$display")
done < "$SSH_PROFILES_FILE" done < "$SSH_PROFILES_FILE"
[[ ${#profiles[@]} -eq 0 ]] && { df_print_info "No profiles"; return 1; }
[[ ${#profiles[@]} -eq 0 ]] && { _ssh_print_info "No profiles saved"; return 1; } local sel=$(printf '%s\n' "${profiles[@]}" | fzf $(df_fzf_opts) --delimiter='|' --with-nth=2 --prompt='SSH > ')
[[ -n "$sel" ]] && ssh-connect "${sel%%|*}"
local selection=$(printf '%s\n' "${profiles[@]}" | \
fzf --height=50% --layout=reverse --border=rounded --prompt='SSH > ' \
--delimiter='|' --with-nth=2)
[[ -n "$selection" ]] && ssh-connect "${selection%%|*}"
} }
ssh-reconnect() { alias sshl='ssh-list' sshs='ssh-save' sshc='ssh-connect' sshd='ssh-delete'
local name="${1:-last}" _ssh_init
if [[ "$name" == "last" ]]; then
local last_profile=$(grep "ssh-connect" "$HISTFILE" 2>/dev/null | tail -1 | awk '{print $2}')
[[ -z "$last_profile" ]] && { _ssh_print_error "No previous connection found"; return 1; }
name="$last_profile"
fi
_ssh_print_info "Reconnecting to: $name"
ssh-connect "$name"
}
ssh-sync-dotfiles() {
local name="$1"
[[ -z "$name" ]] && { echo "Usage: ssh-sync-dotfiles <profile_name>"; return 1; }
local profile_data=$(_ssh_parse_profile "$name")
[[ -z "$profile_data" ]] && { _ssh_print_error "Profile '$name' not found"; return 1; }
IFS='|' read -r connection port key_file ssh_opts description <<< "$profile_data"
local dotfiles_dir="${DOTFILES_DIR:-$HOME/.dotfiles}"
[[ ! -d "$dotfiles_dir" ]] && { _ssh_print_error "Dotfiles directory not found"; return 1; }
df_print_func_name "Sync Dotfiles to: $connection"
local rsync_cmd="rsync -avz --exclude='.git' --exclude='*.local'"
[[ -n "$port" && "$port" != "22" ]] && rsync_cmd="$rsync_cmd -e 'ssh -p $port'"
[[ -n "$key_file" ]] && rsync_cmd="$rsync_cmd -e 'ssh -i $key_file'"
rsync_cmd="$rsync_cmd $dotfiles_dir/ $connection:.dotfiles/"
_ssh_print_info "Running: $rsync_cmd"
if eval "$rsync_cmd"; then
_ssh_print_success "Dotfiles synced successfully"
else
_ssh_print_error "Failed to sync dotfiles"
return 1
fi
}
# ============================================================================
# Aliases
# ============================================================================
alias sshl='ssh-list'
alias sshs='ssh-save'
alias sshc='ssh-connect'
alias sshd='ssh-delete'
alias sshr='ssh-reconnect'
alias sshsync='ssh-sync-dotfiles'
# ============================================================================
# Initialization
# ============================================================================
_ssh_init_profiles

View File

@@ -1,335 +1,124 @@
# ============================================================================ # ============================================================================
# Systemd Integration for Arch/CachyOS # Systemd Integration for Arch/CachyOS
# ============================================================================ # ============================================================================
# Quick shortcuts and helpers for systemd service management
#
# Commands:
# sc <args> - sudo systemctl
# scu <args> - systemctl --user
# scr <service> - restart and show status
# sce <service> - enable and start
# scd <service> - disable and stop
# sclog <service> - follow journal logs
# sc-failed - show failed services
# sc-timers - show active timers
# sc-recent - recently started services
# sc-boot - boot time analysis
# ============================================================================
# Source shared colors (with fallback) source "${0:A:h}/../lib/utils.zsh" 2>/dev/null || \
source "${0:A:h}/../lib/colors.zsh" 2>/dev/null || \ source "$HOME/.dotfiles/zsh/lib/utils.zsh" 2>/dev/null
source "$HOME/.dotfiles/zsh/lib/colors.zsh" 2>/dev/null || {
typeset -g DF_GREEN=$'\033[0;32m' DF_YELLOW=$'\033[1;33m'
typeset -g DF_RED=$'\033[0;31m' DF_BLUE=$'\033[0;34m'
typeset -g DF_CYAN=$'\033[0;36m' DF_NC=$'\033[0m'
}
# ============================================================================ # Core shortcuts
# Core Systemctl Shortcuts sc() { sudo systemctl "$@"; }
# ============================================================================ scu() { systemctl --user "$@"; }
# System-level systemctl (with sudo)
sc() {
sudo systemctl "$@"
}
# User-level systemctl
scu() {
systemctl --user "$@"
}
# Restart service and show status
scr() { scr() {
local service="$1" [[ -z "$1" ]] && { echo "Usage: scr <service>"; return 1; }
[[ -z "$service" ]] && { echo "Usage: scr <service>"; return 1; } df_print_step "Restarting $1..."
sudo systemctl restart "$1" && { df_print_success "Restarted"; sudo systemctl status "$1" --no-pager -l; } || df_print_error "Failed"
echo -e "${DF_BLUE}==>${DF_NC} Restarting ${service}..."
if sudo systemctl restart "$service"; then
echo -e "${DF_GREEN}${DF_NC} Restarted successfully"
echo ""
sudo systemctl status "$service" --no-pager -l
else
echo -e "${DF_RED}${DF_NC} Failed to restart ${service}"
return 1
fi
} }
# Enable and start service
sce() { sce() {
local service="$1" [[ -z "$1" ]] && { echo "Usage: sce <service>"; return 1; }
[[ -z "$service" ]] && { echo "Usage: sce <service>"; return 1; } df_print_step "Enabling $1..."
sudo systemctl enable --now "$1" && { df_print_success "Enabled"; sudo systemctl status "$1" --no-pager -l | head -15; } || df_print_error "Failed"
echo -e "${DF_BLUE}==>${DF_NC} Enabling and starting ${service}..."
if sudo systemctl enable --now "$service"; then
echo -e "${DF_GREEN}${DF_NC} ${service} enabled and started"
sudo systemctl status "$service" --no-pager -l | head -15
else
echo -e "${DF_RED}${DF_NC} Failed to enable ${service}"
return 1
fi
} }
# Disable and stop service
scd() { scd() {
local service="$1" [[ -z "$1" ]] && { echo "Usage: scd <service>"; return 1; }
[[ -z "$service" ]] && { echo "Usage: scd <service>"; return 1; } df_print_step "Disabling $1..."
sudo systemctl disable --now "$1" && df_print_success "Disabled" || df_print_error "Failed"
echo -e "${DF_BLUE}==>${DF_NC} Disabling and stopping ${service}..."
if sudo systemctl disable --now "$service"; then
echo -e "${DF_GREEN}${DF_NC} ${service} disabled and stopped"
else
echo -e "${DF_RED}${DF_NC} Failed to disable ${service}"
return 1
fi
} }
# Follow journal logs for a service
sclog() { sclog() {
local service="$1" [[ -z "$1" ]] && { echo "Usage: sclog <service>"; return 1; }
local lines="${2:-50}" df_print_step "Following logs for $1 (Ctrl+C to exit)..."
[[ -z "$service" ]] && { echo "Usage: sclog <service> [lines]"; return 1; } sudo journalctl -xeu "$1" -f -n "${2:-50}"
echo -e "${DF_BLUE}==>${DF_NC} Following logs for ${service} (Ctrl+C to exit)..."
sudo journalctl -xeu "$service" -f -n "$lines"
} }
# Show recent logs for a service (without follow)
sclogs() { sclogs() {
local service="$1" [[ -z "$1" ]] && { echo "Usage: sclogs <service>"; return 1; }
local lines="${2:-50}" sudo journalctl -xeu "$1" -n "${2:-50}" --no-pager
[[ -z "$service" ]] && { echo "Usage: sclogs <service> [lines]"; return 1; }
sudo journalctl -xeu "$service" -n "$lines" --no-pager
} }
# ============================================================================
# Service Status Commands
# ============================================================================
# Show failed services (system and user)
sc-failed() { sc-failed() {
df_print_func_name "Failed Services" df_print_func_name "Failed Services"
df_print_section "System"
echo -e "${DF_CYAN}System Services:${DF_NC}" local sys=$(systemctl --failed --no-pager --no-legend 2>/dev/null)
local sys_failed=$(systemctl --failed --no-pager --no-legend 2>/dev/null) [[ -z "$sys" ]] && df_print_indent "✓ None" || echo "$sys" | sed 's/^/ /'
if [[ -z "$sys_failed" ]]; then
echo -e " ${DF_GREEN}${DF_NC} No failed system services"
else
echo "$sys_failed" | sed 's/^/ /'
fi
echo -e "\n${DF_CYAN}User Services:${DF_NC}"
local user_failed=$(systemctl --user --failed --no-pager --no-legend 2>/dev/null)
if [[ -z "$user_failed" ]]; then
echo -e " ${DF_GREEN}${DF_NC} No failed user services"
else
echo "$user_failed" | sed 's/^/ /'
fi
echo "" echo ""
df_print_section "User"
local usr=$(systemctl --user --failed --no-pager --no-legend 2>/dev/null)
[[ -z "$usr" ]] && df_print_indent "✓ None" || echo "$usr" | sed 's/^/ /'
} }
# Show active timers
sc-timers() { sc-timers() {
df_print_func_name "Active Timers" df_print_func_name "Active Timers"
df_print_section "System"
echo -e "${DF_CYAN}System Timers:${DF_NC}" systemctl list-timers --no-pager | head -15
systemctl list-timers --no-pager | head -20 echo ""
df_print_section "User"
echo -e "\n${DF_CYAN}User Timers:${DF_NC}"
systemctl --user list-timers --no-pager 2>/dev/null | head -10 systemctl --user list-timers --no-pager 2>/dev/null | head -10
echo ""
} }
# Show recently started/stopped services
sc-recent() {
local count="${1:-15}"
df_print_func_name "Recent Service Activity"
echo -e "${DF_CYAN}Recently Started:${DF_NC}"
systemctl list-units --type=service --state=running --no-pager --no-legend | \
head -"$count" | awk '{print " " $1}'
echo -e "\n${DF_CYAN}Recent Journal (services):${DF_NC}"
journalctl -p 3 -xb --no-pager | tail -"$count" | sed 's/^/ /'
echo ""
}
# Boot time analysis
sc-boot() { sc-boot() {
df_print_func_name "Boot Time Analysis" df_print_func_name "Boot Analysis"
df_print_section "Summary"
echo -e "${DF_CYAN}Boot Summary:${DF_NC}"
systemd-analyze systemd-analyze
echo ""
echo -e "\n${DF_CYAN}Slowest Services (top 10):${DF_NC}" df_print_section "Slowest (top 10)"
systemd-analyze blame --no-pager | head -10 | sed 's/^/ /' systemd-analyze blame --no-pager | head -10 | sed 's/^/ /'
echo -e "\n${DF_CYAN}Critical Chain:${DF_NC}"
systemd-analyze critical-chain --no-pager 2>/dev/null | head -15 | sed 's/^/ /'
echo ""
} }
# ============================================================================
# Service Search and Info
# ============================================================================
# Search for services by name
sc-search() { sc-search() {
local query="$1" [[ -z "$1" ]] && { echo "Usage: sc-search <query>"; return 1; }
[[ -z "$query" ]] && { echo "Usage: sc-search <query>"; return 1; } df_print_func_name "Service Search: $1"
systemctl list-unit-files --type=service --no-pager | grep -i "$1"
df_print_func_name "Service Search: $query"
systemctl list-unit-files --type=service --no-pager | grep -i "$query"
} }
# Show detailed service info
sc-info() { sc-info() {
local service="$1" [[ -z "$1" ]] && { echo "Usage: sc-info <service>"; return 1; }
[[ -z "$service" ]] && { echo "Usage: sc-info <service>"; return 1; } df_print_func_name "Service: $1"
systemctl status "$1" --no-pager -l 2>/dev/null || sudo systemctl status "$1" --no-pager -l
df_print_func_name "Service Info: $service"
echo -e "${DF_CYAN}Status:${DF_NC}"
systemctl status "$service" --no-pager -l 2>/dev/null || \
sudo systemctl status "$service" --no-pager -l
echo -e "\n${DF_CYAN}Unit File:${DF_NC}"
systemctl cat "$service" 2>/dev/null | head -30
echo "" echo ""
df_print_section "Unit File"
systemctl cat "$1" 2>/dev/null | head -30
} }
# ============================================================================ # fzf interactive
# Quick Status for MOTD Integration if df_cmd_exists fzf; then
# ============================================================================
# Get count of failed services (for MOTD/prompt)
_systemd_failed_count() {
local count=$(systemctl --failed --no-pager --no-legend 2>/dev/null | wc -l)
echo "$count"
}
# Check if a service is active (for scripts)
_systemd_is_active() {
local service="$1"
systemctl is-active --quiet "$service" 2>/dev/null
}
# Check if a service is enabled (for scripts)
_systemd_is_enabled() {
local service="$1"
systemctl is-enabled --quiet "$service" 2>/dev/null
}
# ============================================================================
# Interactive Service Management (requires fzf)
# ============================================================================
if command -v fzf &>/dev/null; then
# Interactive service selector
scf() { scf() {
local service=$(systemctl list-units --type=service --no-pager --no-legend | \ local svc=$(systemctl list-units --type=service --no-pager --no-legend | \
awk '{print $1, $2, $3, $4}' | \ fzf $(df_fzf_opts) --prompt='Service > ' --preview='systemctl status {1} --no-pager' | awk '{print $1}')
fzf --height=50% --layout=reverse --border=rounded \ [[ -z "$svc" ]] && return
--prompt='Service > ' \ df_print_info "Selected: $svc"
--preview='systemctl status {1} --no-pager' \ echo "[s]tatus [r]estart [l]ogs [e]nable [d]isable"
--preview-window=right:50%:wrap | \ read -k 1 "act?Action: "; echo
awk '{print $1}') case "$act" in
s) sudo systemctl status "$svc" --no-pager -l ;;
if [[ -n "$service" ]]; then r) scr "$svc" ;;
echo -e "${DF_BLUE}Selected:${DF_NC} $service" l) sclog "$svc" ;;
echo "" e) sce "$svc" ;;
echo "Actions: [s]tatus [r]estart [o]stop [l]ogs [e]nable [d]isable [q]uit" d) scd "$svc" ;;
read -k 1 "action?Action: "
echo ""
case "$action" in
s) sudo systemctl status "$service" --no-pager -l ;;
r) scr "$service" ;;
o) sudo systemctl stop "$service" ;;
l) sclog "$service" ;;
e) sce "$service" ;;
d) scd "$service" ;;
q) return 0 ;;
*) echo "Unknown action" ;;
esac esac
fi
}
# Interactive log viewer
sclogf() {
local service=$(systemctl list-units --type=service --no-pager --no-legend | \
awk '{print $1}' | \
fzf --height=40% --layout=reverse --prompt='Service logs > ')
[[ -n "$service" ]] && sclog "$service"
} }
fi fi
# ============================================================================
# Aliases
# ============================================================================
alias scs='sc status'
alias scstart='sc start'
alias scstop='sc stop'
alias screload='sc daemon-reload'
alias scmask='sc mask'
alias scunmask='sc unmask'
# Journal shortcuts
alias jctl='journalctl'
alias jctlf='journalctl -f'
alias jctlb='journalctl -b'
alias jctlerr='journalctl -p err -b'
# ============================================================================
# Help
# ============================================================================
sc-help() { sc-help() {
df_print_func_name "Systemd Helper Commands" df_print_func_name "Systemd Commands"
cat << 'EOF' cat << 'EOF'
sc <args> sudo systemctl
Core Commands: scu <args> systemctl --user
sc <args> sudo systemctl <args> scr <svc> Restart + status
scu <args> systemctl --user <args> sce <svc> Enable + start
scr <service> Restart and show status scd <svc> Disable + stop
sce <service> Enable and start (--now) sclog <svc> Follow logs
scd <service> Disable and stop (--now) sclogs <svc> Recent logs
sclog <service> Follow journal logs (-f) sc-failed Failed services
sclogs <service> Show recent logs (no follow) sc-timers Active timers
sc-boot Boot analysis
Status Commands: sc-search <q> Search services
sc-failed Show failed services sc-info <svc> Service details
sc-timers Show active timers scf Interactive (fzf)
sc-recent Recently started services
sc-boot Boot time analysis
sc-search <query> Search services by name
sc-info <service> Detailed service info
Interactive (requires fzf):
scf Interactive service manager
sclogf Interactive log viewer
Aliases:
scs sc status
scstart sc start
scstop sc stop
screload sc daemon-reload
Journal:
jctl journalctl
jctlf journalctl -f
jctlb journalctl -b (current boot)
jctlerr journalctl -p err -b
EOF EOF
} }
alias scs='sc status' scstart='sc start' scstop='sc stop' screload='sc daemon-reload'
alias jctl='journalctl' jctlf='journalctl -f' jctlb='journalctl -b'

View File

@@ -1,380 +1,116 @@
# ============================================================================ # ============================================================================
# Tmux Workspace Manager - Project Templates & Layouts # Tmux Workspace Manager - Project Templates & Layouts
# ============================================================================ # ============================================================================
# Quick project workspace setup with pre-configured tmux layouts
# ============================================================================
# Source shared colors (with fallback) source "${0:A:h}/../lib/utils.zsh" 2>/dev/null || \
source "${0:A:h}/../lib/colors.zsh" 2>/dev/null || \ source "$HOME/.dotfiles/zsh/lib/utils.zsh" 2>/dev/null
source "$HOME/.dotfiles/zsh/lib/colors.zsh" 2>/dev/null || {
typeset -g DF_GREEN=$'\033[0;32m' DF_BLUE=$'\033[0;34m' typeset -g TW_TEMPLATES="${TW_TEMPLATES:-$HOME/.dotfiles/.tmux-templates}"
typeset -g DF_YELLOW=$'\033[1;33m' DF_CYAN=$'\033[0;36m' typeset -g TW_PREFIX="${TW_PREFIX:-work}"
typeset -g DF_RED=$'\033[0;31m' DF_NC=$'\033[0m' typeset -g TW_DEFAULT="${TW_DEFAULT:-dev}"
_tw_check() { df_require_cmd tmux || return 1; }
_tw_init() {
df_ensure_dir "$TW_TEMPLATES"
[[ ! -f "$TW_TEMPLATES/dev.tmux" ]] && {
echo -e "# Dev layout\nsplit-window -h -p 50\nsplit-window -v -p 50\nselect-pane -t 0" > "$TW_TEMPLATES/dev.tmux"
echo -e "# Ops layout\nsplit-window -h\nsplit-window -v\nselect-pane -t 0\nsplit-window -v\nselect-pane -t 0" > "$TW_TEMPLATES/ops.tmux"
echo "# Full\n" > "$TW_TEMPLATES/full.tmux"
df_print_success "Created default templates"
} }
# ============================================================================
# Configuration
# ============================================================================
typeset -g TW_TEMPLATES_DIR="${TW_TEMPLATES_DIR:-$HOME/.dotfiles/.tmux-templates}"
typeset -g TW_SESSION_PREFIX="${TW_SESSION_PREFIX:-work}"
typeset -g TW_DEFAULT_TEMPLATE="${TW_DEFAULT_TEMPLATE:-dev}"
# ============================================================================
# Helper Functions
# ============================================================================
_tw_print_step() { echo -e "${DF_BLUE}==>${DF_NC} $1"; }
_tw_print_success() { echo -e "${DF_GREEN}${DF_NC} $1"; }
_tw_print_error() { echo -e "${DF_RED}${DF_NC} $1"; }
_tw_print_info() { echo -e "${DF_CYAN}${DF_NC} $1"; }
_tw_check_tmux() {
if ! command -v tmux &>/dev/null; then
_tw_print_error "tmux not installed"
return 1
fi
return 0
} }
_tw_init_templates() {
mkdir -p "$TW_TEMPLATES_DIR"
[[ ! -f "$TW_TEMPLATES_DIR/dev.tmux" ]] && _tw_create_default_templates
}
# ============================================================================
# Default Template Definitions
# ============================================================================
_tw_create_default_templates() {
_tw_print_step "Creating default templates..."
cat > "$TW_TEMPLATES_DIR/dev.tmux" << 'EOF'
# Development workspace
split-window -h -p 50
split-window -v -p 50
select-pane -t 0
EOF
cat > "$TW_TEMPLATES_DIR/ops.tmux" << 'EOF'
# Operations workspace - 4 panes
split-window -h -p 50
split-window -v -p 50
select-pane -t 0
split-window -v -p 50
select-pane -t 0
EOF
cat > "$TW_TEMPLATES_DIR/ssh-multi.tmux" << 'EOF'
# Multi-server SSH workspace
split-window -h -p 50
split-window -v -p 50
select-pane -t 0
split-window -v -p 50
select-pane -t 0
EOF
cat > "$TW_TEMPLATES_DIR/debug.tmux" << 'EOF'
# Debug workspace
split-window -h -p 30
select-pane -t 0
EOF
cat > "$TW_TEMPLATES_DIR/full.tmux" << 'EOF'
# Full workspace - single pane
EOF
cat > "$TW_TEMPLATES_DIR/review.tmux" << 'EOF'
# Code Review workspace
split-window -h -p 50
select-pane -t 0
EOF
_tw_print_success "Created default templates in: $TW_TEMPLATES_DIR"
}
# ============================================================================
# Template Management
# ============================================================================
tw-templates() { tw-templates() {
_tw_init_templates _tw_init
df_print_func_name "Tmux Templates"
df_print_func_name "Available Tmux Templates" for t in "$TW_TEMPLATES"/*.tmux; do
[[ -f "$t" ]] && df_print_indent "$(basename "$t" .tmux)"
for template in "$TW_TEMPLATES_DIR"/*.tmux; do
[[ ! -f "$template" ]] && continue
local name=$(basename "$template" .tmux)
local description=$(grep "^#" "$template" | head -2 | tail -1 | sed 's/^# *//')
echo -e "${DF_GREEN}${DF_NC} ${DF_CYAN}$name${DF_NC}"
[[ -n "$description" ]] && echo " $description"
done done
echo ""
echo df_print_info "Create: tw-create <name> <template>"
echo "Create workspace: ${DF_CYAN}tw-create myproject dev${DF_NC}"
echo "Quick attach: ${DF_CYAN}tw myproject${DF_NC}"
} }
tw-template-edit() {
local template_name="$1"
[[ -z "$template_name" ]] && { echo "Usage: tw-template-edit <template_name>"; tw-templates; return 1; }
_tw_init_templates
${EDITOR:-vim} "$TW_TEMPLATES_DIR/${template_name}.tmux"
_tw_print_success "Template edited: $template_name"
}
# ============================================================================
# Workspace Management
# ============================================================================
tw-create() { tw-create() {
local workspace_name="$1" local name="$1" tmpl="${2:-$TW_DEFAULT}"
local template="${2:-$TW_DEFAULT_TEMPLATE}" [[ -z "$name" ]] && { tw-templates; return 1; }
_tw_check || return 1
_tw_init
[[ -z "$workspace_name" ]] && { echo "Usage: tw-create <workspace_name> [template]"; tw-templates; return 1; } local session="${TW_PREFIX}-${name}"
tmux has-session -t "$session" 2>/dev/null && { df_print_error "'$name' exists"; return 1; }
_tw_check_tmux || return 1 local tfile="$TW_TEMPLATES/${tmpl}.tmux"
_tw_init_templates [[ ! -f "$tfile" ]] && { df_print_error "Template '$tmpl' not found"; tw-templates; return 1; }
local session_name="${TW_SESSION_PREFIX}-${workspace_name}" df_print_step "Creating: $name (template: $tmpl)"
tmux new-session -d -s "$session"
tmux source-file "$tfile" -t "$session"
if tmux has-session -t "$session_name" 2>/dev/null; then df_in_git_repo && {
_tw_print_error "Workspace '$workspace_name' already exists" local root=$(df_git_root)
echo "Use: ${DF_CYAN}tw $workspace_name${DF_NC} to attach" df_print_info "Git root: $root"
return 1 tmux send-keys -t "$session:0" "cd $root" C-m
fi
local template_file="$TW_TEMPLATES_DIR/${template}.tmux"
if [[ ! -f "$template_file" ]]; then
_tw_print_error "Template '$template' not found"
tw-templates
return 1
fi
_tw_print_step "Creating workspace: $workspace_name (template: $template)"
tmux new-session -d -s "$session_name"
_tw_print_step "Applying template: $template"
tmux source-file "$template_file" -t "$session_name"
if git rev-parse --git-dir &>/dev/null 2>&1; then
local git_root=$(git rev-parse --show-toplevel)
_tw_print_info "Setting workspace directory to: $git_root"
tmux send-keys -t "$session_name:0" "cd $git_root" C-m
fi
_tw_print_success "Workspace created: $workspace_name"
if [[ -z "$TMUX" ]]; then
_tw_print_step "Attaching to workspace..."
tmux attach-session -t "$session_name"
else
_tw_print_info "Switch with: ${DF_CYAN}tmux switch-client -t $session_name${DF_NC}"
fi
} }
tw-attach() { df_print_success "Created: $name"
local workspace_name="$1" [[ -z "$TMUX" ]] && tmux attach -t "$session" || df_print_info "Switch: tmux switch -t $session"
[[ -z "$workspace_name" ]] && { echo "Usage: tw-attach <workspace_name>"; tw-list; return 1; }
_tw_check_tmux || return 1
local session_name="${TW_SESSION_PREFIX}-${workspace_name}"
if ! tmux has-session -t "$session_name" 2>/dev/null; then
_tw_print_error "Workspace '$workspace_name' not found"
echo "Create it with: ${DF_CYAN}tw-create $workspace_name${DF_NC}"
return 1
fi
if [[ -z "$TMUX" ]]; then
tmux attach-session -t "$session_name"
else
tmux switch-client -t "$session_name"
fi
} }
tw-list() { tw-list() {
_tw_check_tmux || return 1 _tw_check || return 1
df_print_func_name "Tmux Workspaces"
df_print_func_name "Active Tmux Workspaces" local found=false
tmux list-sessions 2>/dev/null | while IFS=: read -r sess rest; do
local has_workspaces=false [[ "$sess" == ${TW_PREFIX}-* ]] && { found=true; df_print_indent "${sess#${TW_PREFIX}-}"; }
tmux list-sessions 2>/dev/null | while IFS=: read -r session_full rest; do
if [[ "$session_full" == ${TW_SESSION_PREFIX}-* ]]; then
has_workspaces=true
local workspace_name="${session_full#${TW_SESSION_PREFIX}-}"
local attached=""
if [[ -n "$TMUX" ]]; then
local current_session=$(tmux display-message -p '#S')
[[ "$current_session" == "$session_full" ]] && attached=" ${DF_GREEN}(current)${DF_NC}"
fi
echo -e "${DF_GREEN}${DF_NC} ${DF_CYAN}$workspace_name${DF_NC}$attached"
echo " Session: $session_full"
fi
done done
[[ "$found" != true ]] && df_print_info "No workspaces. Use: tw-create <name>"
}
if [[ "$has_workspaces" != true ]]; then tw-attach() {
_tw_print_info "No active workspaces" local name="$1"
echo "Create one with: ${DF_CYAN}tw-create myproject${DF_NC}" [[ -z "$name" ]] && { tw-list; return 1; }
fi _tw_check || return 1
local session="${TW_PREFIX}-${name}"
tmux has-session -t "$session" 2>/dev/null || { df_print_error "'$name' not found"; return 1; }
[[ -z "$TMUX" ]] && tmux attach -t "$session" || tmux switch -t "$session"
} }
tw-delete() { tw-delete() {
local workspace_name="$1" [[ -z "$1" ]] && { tw-list; return 1; }
[[ -z "$workspace_name" ]] && { echo "Usage: tw-delete <workspace_name>"; tw-list; return 1; } _tw_check || return 1
local session="${TW_PREFIX}-${1}"
_tw_check_tmux || return 1 tmux has-session -t "$session" 2>/dev/null || { df_print_error "'$1' not found"; return 1; }
tmux kill-session -t "$session"
local session_name="${TW_SESSION_PREFIX}-${workspace_name}" df_print_success "Deleted: $1"
if ! tmux has-session -t "$session_name" 2>/dev/null; then
_tw_print_error "Workspace '$workspace_name' not found"
return 1
fi
tmux kill-session -t "$session_name"
_tw_print_success "Deleted workspace: $workspace_name"
}
tw-save() {
local template_name="$1"
[[ -z "$template_name" ]] && { echo "Usage: tw-save <template_name>"; return 1; }
_tw_check_tmux || return 1
[[ -z "$TMUX" ]] && { _tw_print_error "Must be run from inside tmux"; return 1; }
_tw_init_templates
local template_file="$TW_TEMPLATES_DIR/${template_name}.tmux"
[[ -f "$template_file" ]] && {
read -q "REPLY?Template '$template_name' exists. Overwrite? [y/N]: "
echo
[[ ! "$REPLY" =~ ^[Yy]$ ]] && return 1
}
_tw_print_step "Saving current layout as template: $template_name"
local pane_count=$(tmux display-message -p '#{window_panes}')
cat > "$template_file" << EOF
# Custom template: $template_name
# Saved: $(date)
# Panes: $pane_count
EOF
if (( pane_count == 2 )); then
echo "split-window -h -p 50" >> "$template_file"
elif (( pane_count == 3 )); then
echo "split-window -h -p 50" >> "$template_file"
echo "split-window -v -p 50" >> "$template_file"
elif (( pane_count == 4 )); then
echo "split-window -h -p 50" >> "$template_file"
echo "split-window -v -p 50" >> "$template_file"
echo "select-pane -t 0" >> "$template_file"
echo "split-window -v -p 50" >> "$template_file"
fi
echo "" >> "$template_file"
echo "select-pane -t 0" >> "$template_file"
_tw_print_success "Template saved: $template_name"
echo " File: $template_file"
echo " Edit: ${DF_CYAN}tw-template-edit $template_name${DF_NC}"
} }
tw() { tw() {
local workspace_name="$1" local name="$1" tmpl="${2:-$TW_DEFAULT}"
local template="${2:-$TW_DEFAULT_TEMPLATE}" [[ -z "$name" ]] && { tw-list; return; }
_tw_check || return 1
[[ -z "$workspace_name" ]] && { tw-list; return 0; } local session="${TW_PREFIX}-${name}"
tmux has-session -t "$session" 2>/dev/null && tw-attach "$name" || tw-create "$name" "$tmpl"
_tw_check_tmux || return 1
local session_name="${TW_SESSION_PREFIX}-${workspace_name}"
if tmux has-session -t "$session_name" 2>/dev/null; then
tw-attach "$workspace_name"
else
_tw_print_info "Workspace doesn't exist. Creating with template: $template"
tw-create "$workspace_name" "$template"
fi
} }
twf() { twf() {
if ! command -v fzf &>/dev/null; then df_require_cmd fzf || return 1
_tw_print_error "fzf not installed" _tw_check || return 1
return 1
fi
_tw_check_tmux || return 1
local sessions=() local sessions=()
tmux list-sessions 2>/dev/null | while IFS=: read -r session_full rest; do tmux list-sessions 2>/dev/null | while IFS=: read -r sess rest; do
if [[ "$session_full" == ${TW_SESSION_PREFIX}-* ]]; then [[ "$sess" == ${TW_PREFIX}-* ]] && sessions+=("${sess#${TW_PREFIX}-}")
local workspace_name="${session_full#${TW_SESSION_PREFIX}-}"
sessions+=("$workspace_name")
fi
done done
[[ ${#sessions[@]} -eq 0 ]] && { df_print_info "No workspaces"; return 1; }
[[ ${#sessions[@]} -eq 0 ]] && { _tw_print_info "No workspaces found"; return 1; } local sel=$(printf '%s\n' "${sessions[@]}" | fzf $(df_fzf_opts) --prompt='Workspace > ')
[[ -n "$sel" ]] && tw-attach "$sel"
local selection=$(printf '%s\n' "${sessions[@]}" | \
fzf --height=40% --layout=reverse --border=rounded --prompt='Workspace > ')
[[ -n "$selection" ]] && tw-attach "$selection"
} }
tw-sync() { tw-sync() {
[[ -z "$TMUX" ]] && { _tw_print_error "Must be run from inside tmux"; return 1; } [[ -z "$TMUX" ]] && { df_print_error "Must be in tmux"; return 1; }
local cur=$(tmux show-window-option -v synchronize-panes 2>/dev/null)
local current=$(tmux show-window-option -v synchronize-panes 2>/dev/null) [[ "$cur" == "on" ]] && { tmux set-window-option synchronize-panes off; df_print_info "Sync: OFF"; } || \
{ tmux set-window-option synchronize-panes on; df_print_info "Sync: ON"; }
if [[ "$current" == "on" ]]; then
tmux set-window-option synchronize-panes off
_tw_print_info "Pane synchronization: ${DF_RED}OFF${DF_NC}"
else
tmux set-window-option synchronize-panes on
_tw_print_info "Pane synchronization: ${DF_GREEN}ON${DF_NC}"
fi
} }
tw-rename() { alias twl='tw-list' twc='tw-create' twa='tw-attach' twd='tw-delete' twt='tw-templates'
local old_name="$1" _tw_init
local new_name="$2"
[[ -z "$old_name" || -z "$new_name" ]] && { echo "Usage: tw-rename <old_name> <new_name>"; return 1; }
_tw_check_tmux || return 1
local old_session="${TW_SESSION_PREFIX}-${old_name}"
local new_session="${TW_SESSION_PREFIX}-${new_name}"
if ! tmux has-session -t "$old_session" 2>/dev/null; then
_tw_print_error "Workspace '$old_name' not found"
return 1
fi
tmux rename-session -t "$old_session" "$new_session"
_tw_print_success "Renamed: $old_name$new_name"
}
# ============================================================================
# Aliases
# ============================================================================
alias twl='tw-list'
alias twc='tw-create'
alias twa='tw-attach'
alias twd='tw-delete'
alias tws='tw-save'
alias twt='tw-templates'
alias twe='tw-template-edit'
# ============================================================================
# Initialization
# ============================================================================
_tw_init_templates

View File

@@ -2,114 +2,52 @@
# Shared Utility Functions for Zsh Dotfiles # Shared Utility Functions for Zsh Dotfiles
# ============================================================================ # ============================================================================
# Common helper functions used across multiple function files # Common helper functions used across multiple function files
# Note: colors.zsh provides: DF_* color variables and df_print_func_name
# #
# Source this file in your .zshrc or in individual function files: # Source this file in function files:
# source "$HOME/.dotfiles/zsh/lib/utils.zsh" # source "${0:A:h}/../lib/utils.zsh"
#
# Provides:
# - Standardized output formatting (step/success/error/warning/info)
# - Command dependency checking
# - User confirmation prompts
# - Common file/directory operations
# ============================================================================ # ============================================================================
# Ensure colors are loaded first # Ensure colors are loaded first (provides DF_* vars and df_print_func_name)
source "${0:A:h}/colors.zsh" 2>/dev/null || { source "${0:A:h}/colors.zsh" 2>/dev/null || \
typeset -g DF_GREEN=$'\033[0;32m' DF_YELLOW=$'\033[1;33m' source "$HOME/.dotfiles/zsh/lib/colors.zsh" 2>/dev/null
typeset -g DF_RED=$'\033[0;31m' DF_BLUE=$'\033[0;34m'
typeset -g DF_CYAN=$'\033[0;36m' DF_DIM=$'\033[2m' DF_NC=$'\033[0m'
}
# ============================================================================ # ============================================================================
# Output Formatting Functions # Output Formatting Functions
# ============================================================================ # ============================================================================
# These provide consistent, styled output across all dotfiles functions.
# Use these instead of raw echo statements for a unified look.
# Print a step/action indicator (blue arrow) # Print a step/action indicator (blue arrow)
# Usage: df_print_step "Installing packages" df_print_step() { echo -e "${DF_BLUE}==>${DF_NC} $1"; }
df_print_step() {
echo -e "${DF_BLUE}==>${DF_NC} $1"
}
# Print a success message (green checkmark) # Print a success message (green checkmark)
# Usage: df_print_success "Installation complete" df_print_success() { echo -e "${DF_GREEN}${DF_NC} $1"; }
df_print_success() {
echo -e "${DF_GREEN}${DF_NC} $1"
}
# Print an error message (red X) # Print an error message (red X)
# Usage: df_print_error "Failed to connect" df_print_error() { echo -e "${DF_RED}${DF_NC} $1"; }
df_print_error() {
echo -e "${DF_RED}${DF_NC} $1"
}
# Print a warning message (yellow warning sign) # Print a warning message (yellow warning sign)
# Usage: df_print_warning "Config file missing, using defaults" df_print_warning() { echo -e "${DF_YELLOW}${DF_NC} $1"; }
df_print_warning() {
echo -e "${DF_YELLOW}${DF_NC} $1"
}
# Print an info message (cyan info icon) # Print an info message (cyan info icon)
# Usage: df_print_info "Using cached version" df_print_info() { echo -e "${DF_CYAN}${DF_NC} $1"; }
df_print_info() {
echo -e "${DF_CYAN}${DF_NC} $1"
}
# Print a section header (cyan, for grouping output) # Print a section header (cyan label)
# Usage: df_print_section "Configuration" df_print_section() { echo -e "${DF_CYAN}$1:${DF_NC}"; }
df_print_section() {
echo -e "${DF_CYAN}$1:${DF_NC}"
}
# Print a bullet point item (green dot) # Print indented content
# Usage: df_print_item "my-alias" "git status" df_print_indent() { echo " $1"; }
df_print_item() {
local name="$1"
local description="${2:-}"
if [[ -n "$description" ]]; then
echo -e " ${DF_GREEN}${DF_NC} ${DF_CYAN}$name${DF_NC} - $description"
else
echo -e " ${DF_GREEN}${DF_NC} ${DF_CYAN}$name${DF_NC}"
fi
}
# Print indented content (for sub-items)
# Usage: df_print_indent "Additional details here"
df_print_indent() {
echo " $1"
}
# Print a function name header (box style) - already defined in colors.zsh
# but ensure it's available
if ! typeset -f df_print_func_name &>/dev/null; then
df_print_func_name() {
local name="$1"
local width=$((${#name} + 4))
local border=$(printf '─%.0s' $(seq 1 $width))
echo -e "${DF_CYAN}${border}${DF_NC}"
echo -e "${DF_CYAN}${DF_NC} $name ${DF_CYAN}${DF_NC}"
echo -e "${DF_CYAN}${border}${DF_NC}"
}
fi
# ============================================================================ # ============================================================================
# Command Dependency Checking # Command Dependency Checking
# ============================================================================ # ============================================================================
# Check if required commands are available before running functions.
# Check if a command exists # Check if a command exists
# Usage: df_cmd_exists git && echo "git is installed" df_cmd_exists() { command -v "$1" &>/dev/null; }
df_cmd_exists() {
command -v "$1" &>/dev/null
}
# Require a command, exit with error if missing # Require a command, show install hint if missing
# Usage: df_require_cmd fzf || return 1
# Usage: df_require_cmd compsize "compsize" || return 1
df_require_cmd() { df_require_cmd() {
local cmd="$1" local cmd="$1"
local package="${2:-$1}" # Default to command name as package name local package="${2:-$1}"
if ! command -v "$cmd" &>/dev/null; then if ! command -v "$cmd" &>/dev/null; then
df_print_error "$cmd not installed" df_print_error "$cmd not installed"
@@ -119,139 +57,55 @@ df_require_cmd() {
return 0 return 0
} }
# Require multiple commands at once
# Usage: df_require_cmds git fzf tmux || return 1
df_require_cmds() {
local missing=()
for cmd in "$@"; do
if ! command -v "$cmd" &>/dev/null; then
missing+=("$cmd")
fi
done
if [[ ${#missing[@]} -gt 0 ]]; then
df_print_error "Missing required commands: ${missing[*]}"
echo "Install: sudo pacman -S ${missing[*]}"
return 1
fi
return 0
}
# ============================================================================ # ============================================================================
# User Confirmation Prompts # User Confirmation
# ============================================================================ # ============================================================================
# Consistent confirmation dialogs for dangerous operations.
# Ask for yes/no confirmation (defaults to no) # Ask for yes/no confirmation (defaults to no)
# Usage: df_confirm "Delete all files?" || return
df_confirm() { df_confirm() {
local prompt="$1" local prompt="$1"
read -q "REPLY?$prompt [y/N]: " read -q "REPLY?$prompt [y/N]: "
echo # Newline after response echo
[[ "$REPLY" =~ ^[Yy]$ ]] [[ "$REPLY" =~ ^[Yy]$ ]]
} }
# Ask for confirmation with a warning prefix # Confirm with warning prefix
# Usage: df_confirm_warning "This will delete all data" || return
df_confirm_warning() { df_confirm_warning() {
local message="$1" df_print_warning "$1"
df_print_warning "$message"
df_confirm "Continue?" df_confirm "Continue?"
} }
# ============================================================================ # ============================================================================
# File and Directory Helpers # File/Directory Helpers
# ============================================================================ # ============================================================================
# Check if running inside a git repository # Check if in a git repo
# Usage: df_in_git_repo && echo "In a git repo" df_in_git_repo() { git rev-parse --git-dir &>/dev/null 2>&1; }
df_in_git_repo() {
git rev-parse --git-dir &>/dev/null 2>&1
}
# Get git root directory # Get git root directory
# Usage: local root=$(df_git_root) df_git_root() { git rev-parse --show-toplevel 2>/dev/null; }
df_git_root() {
git rev-parse --show-toplevel 2>/dev/null
}
# Ensure a directory exists, create if missing # Ensure directory exists
# Usage: df_ensure_dir "$HOME/.cache/myapp" df_ensure_dir() { [[ ! -d "$1" ]] && mkdir -p "$1"; }
df_ensure_dir() {
local dir="$1"
[[ ! -d "$dir" ]] && mkdir -p "$dir"
}
# Ensure a file exists with optional default content # Ensure file exists with optional default content
# Usage: df_ensure_file "$HOME/.config/myapp/config" "# Default config"
df_ensure_file() { df_ensure_file() {
local file="$1" local file="$1" content="${2:-}"
local default_content="${2:-}"
if [[ ! -f "$file" ]]; then if [[ ! -f "$file" ]]; then
df_ensure_dir "$(dirname "$file")" df_ensure_dir "$(dirname "$file")"
if [[ -n "$default_content" ]]; then [[ -n "$content" ]] && echo "$content" > "$file" || touch "$file"
echo "$default_content" > "$file"
else
touch "$file"
fi
fi fi
} }
# ============================================================================ # ============================================================================
# String Helpers # Environment Checks
# ============================================================================ # ============================================================================
# Truncate a string to a maximum length df_in_tmux() { [[ -n "$TMUX" ]]; }
# Usage: df_truncate "Long string here" 10 # "Long st..." df_is_btrfs() { [[ "$(df -T / 2>/dev/null | awk 'NR==2 {print $2}')" == "btrfs" ]]; }
df_truncate() {
local str="$1"
local max="${2:-50}"
if [[ ${#str} -gt $max ]]; then
echo "${str:0:$((max-3))}..."
else
echo "$str"
fi
}
# Pad a string to a minimum length
# Usage: df_pad "short" 10 # "short "
df_pad() {
local str="$1"
local width="${2:-20}"
printf "%-${width}s" "$str"
}
# ============================================================================ # ============================================================================
# FZF Helpers # FZF Helpers
# ============================================================================ # ============================================================================
# Standard fzf options for consistent look df_fzf_opts() { echo "--height=50% --layout=reverse --border=rounded"; }
df_fzf_opts() {
echo "--height=50% --layout=reverse --border=rounded"
}
# Run fzf with standard options
# Usage: echo -e "opt1\nopt2" | df_fzf "Select > "
df_fzf() {
local prompt="${1:-Select > }"
shift
fzf $(df_fzf_opts) --prompt="$prompt" "$@"
}
# ============================================================================
# Environment Helpers
# ============================================================================
# Check if inside tmux
# Usage: df_in_tmux && echo "Inside tmux"
df_in_tmux() {
[[ -n "$TMUX" ]]
}
# Check if root filesystem is btrfs
# Usage: df_is_btrfs && echo "Using btrfs"
df_is_btrfs() {
[[ "$(df -T / | awk 'NR==2 {print $2}')" == "btrfs" ]]
}