Dotfiles update 2025-12-25 10:23
This commit is contained in:
@@ -2,429 +2,209 @@
|
||||
# Btrfs Helpers for Arch/CachyOS
|
||||
# ============================================================================
|
||||
# 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/colors.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
|
||||
# ============================================================================
|
||||
source "${0:A:h}/../lib/utils.zsh" 2>/dev/null || \
|
||||
source "$HOME/.dotfiles/zsh/lib/utils.zsh" 2>/dev/null
|
||||
|
||||
typeset -g BTRFS_DEFAULT_MOUNT="${BTRFS_DEFAULT_MOUNT:-/}"
|
||||
|
||||
# ============================================================================
|
||||
# Detection
|
||||
# ============================================================================
|
||||
|
||||
_btrfs_check() {
|
||||
if ! command -v btrfs &>/dev/null; then
|
||||
echo -e "${DF_RED}✗${DF_NC} btrfs-progs not installed"
|
||||
echo "Install: sudo pacman -S btrfs-progs"
|
||||
df_require_cmd btrfs btrfs-progs || return 1
|
||||
if ! df_is_btrfs; then
|
||||
df_print_warning "Root filesystem is not btrfs"
|
||||
return 1
|
||||
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
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Core Commands
|
||||
# ============================================================================
|
||||
|
||||
# Show filesystem usage
|
||||
btrfs-usage() {
|
||||
_btrfs_check || return 1
|
||||
local mount="${1:-$BTRFS_DEFAULT_MOUNT}"
|
||||
|
||||
df_print_func_name "Btrfs Filesystem Usage: ${mount}"
|
||||
|
||||
sudo btrfs filesystem usage "$mount" -h
|
||||
}
|
||||
|
||||
# List all subvolumes
|
||||
btrfs-subs() {
|
||||
_btrfs_check || return 1
|
||||
local mount="${1:-$BTRFS_DEFAULT_MOUNT}"
|
||||
|
||||
df_print_func_name "Btrfs Subvolumes"
|
||||
|
||||
echo -e "${DF_CYAN}Subvolume List:${DF_NC}"
|
||||
df_print_section "Subvolume List"
|
||||
sudo btrfs subvolume list "$mount" | while read -r line; do
|
||||
local path=$(echo "$line" | awk '{print $NF}')
|
||||
local id=$(echo "$line" | awk '{print $2}')
|
||||
echo -e " ${DF_GREEN}●${DF_NC} [$id] $path"
|
||||
df_print_indent "● [$id] $path"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo -e "${DF_CYAN}Default Subvolume:${DF_NC}"
|
||||
df_print_section "Default Subvolume"
|
||||
sudo btrfs subvolume get-default "$mount"
|
||||
}
|
||||
|
||||
# Start balance operation
|
||||
btrfs-balance() {
|
||||
_btrfs_check || return 1
|
||||
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"
|
||||
|
||||
echo -e "${DF_YELLOW}⚠${DF_NC} This may take a while and use significant I/O"
|
||||
df_confirm_warning "This may take a while and use significant I/O" || return 0
|
||||
echo ""
|
||||
|
||||
read -q "REPLY?Continue? [y/N]: "; echo
|
||||
[[ ! "$REPLY" =~ ^[Yy]$ ]] && return 0
|
||||
|
||||
echo ""
|
||||
echo -e "${DF_BLUE}==>${DF_NC} Balancing data chunks with <${usage}% usage..."
|
||||
df_print_step "Balancing data chunks with <${usage}% usage..."
|
||||
sudo btrfs balance start -dusage="$usage" -musage="$usage" "$mount" -v
|
||||
|
||||
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
|
||||
[[ $? -eq 0 ]] && df_print_success "Balance completed" || df_print_warning "Balance finished (may have been interrupted)"
|
||||
}
|
||||
|
||||
# Check balance status
|
||||
btrfs-balance-status() {
|
||||
_btrfs_check || return 1
|
||||
local mount="${1:-$BTRFS_DEFAULT_MOUNT}"
|
||||
|
||||
df_print_func_name "Btrfs Balance Status"
|
||||
|
||||
sudo btrfs balance status "$mount"
|
||||
sudo btrfs balance status "${1:-$BTRFS_DEFAULT_MOUNT}"
|
||||
}
|
||||
|
||||
# Cancel running balance
|
||||
btrfs-balance-cancel() {
|
||||
_btrfs_check || return 1
|
||||
local mount="${1:-$BTRFS_DEFAULT_MOUNT}"
|
||||
|
||||
echo -e "${DF_BLUE}==>${DF_NC} Cancelling balance on ${mount}..."
|
||||
sudo btrfs balance cancel "$mount"
|
||||
df_print_step "Cancelling balance..."
|
||||
sudo btrfs balance cancel "${1:-$BTRFS_DEFAULT_MOUNT}"
|
||||
}
|
||||
|
||||
# Start scrub operation
|
||||
btrfs-scrub() {
|
||||
_btrfs_check || return 1
|
||||
local mount="${1:-$BTRFS_DEFAULT_MOUNT}"
|
||||
|
||||
df_print_func_name "Btrfs Scrub"
|
||||
|
||||
# Check if scrub is already running
|
||||
local status=$(sudo btrfs scrub status "$mount" 2>/dev/null)
|
||||
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/^/ /'
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo -e "${DF_YELLOW}⚠${DF_NC} Scrub verifies data integrity and may take hours"
|
||||
read -q "REPLY?Start scrub? [y/N]: "; echo
|
||||
[[ ! "$REPLY" =~ ^[Yy]$ ]] && return 0
|
||||
|
||||
echo ""
|
||||
echo -e "${DF_BLUE}==>${DF_NC} Starting scrub..."
|
||||
df_print_warning "Scrub verifies data integrity and may take hours"
|
||||
df_confirm "Start scrub?" || return 0
|
||||
df_print_step "Starting scrub..."
|
||||
sudo btrfs scrub start "$mount"
|
||||
|
||||
echo ""
|
||||
echo -e "${DF_CYAN}Scrub Status:${DF_NC}"
|
||||
df_print_section "Scrub Status"
|
||||
sudo btrfs scrub status "$mount"
|
||||
|
||||
echo ""
|
||||
echo -e "${DF_CYAN}Monitor with:${DF_NC} btrfs-scrub-status"
|
||||
df_print_info "Monitor with: btrfs-scrub-status"
|
||||
}
|
||||
|
||||
# Show scrub status
|
||||
btrfs-scrub-status() {
|
||||
_btrfs_check || return 1
|
||||
local mount="${1:-$BTRFS_DEFAULT_MOUNT}"
|
||||
|
||||
df_print_func_name "Btrfs Scrub Status"
|
||||
|
||||
sudo btrfs scrub status "$mount"
|
||||
sudo btrfs scrub status "${1:-$BTRFS_DEFAULT_MOUNT}"
|
||||
}
|
||||
|
||||
# Cancel scrub
|
||||
btrfs-scrub-cancel() {
|
||||
_btrfs_check || return 1
|
||||
local mount="${1:-$BTRFS_DEFAULT_MOUNT}"
|
||||
|
||||
echo -e "${DF_BLUE}==>${DF_NC} Cancelling scrub on ${mount}..."
|
||||
sudo btrfs scrub cancel "$mount"
|
||||
df_print_step "Cancelling scrub..."
|
||||
sudo btrfs scrub cancel "${1:-$BTRFS_DEFAULT_MOUNT}"
|
||||
}
|
||||
|
||||
# Defragment file or directory
|
||||
btrfs-defrag() {
|
||||
_btrfs_check || return 1
|
||||
local target="${1:-.}"
|
||||
|
||||
if [[ ! -e "$target" ]]; then
|
||||
echo -e "${DF_RED}✗${DF_NC} Target not found: $target"
|
||||
return 1
|
||||
fi
|
||||
|
||||
[[ ! -e "$target" ]] && { df_print_error "Target not found: $target"; return 1; }
|
||||
df_print_func_name "Btrfs Defragment"
|
||||
|
||||
if [[ -d "$target" ]]; then
|
||||
echo -e "${DF_YELLOW}⚠${DF_NC} Recursive defrag on directory: $target"
|
||||
read -q "REPLY?Continue? [y/N]: "; echo
|
||||
[[ ! "$REPLY" =~ ^[Yy]$ ]] && return 0
|
||||
|
||||
df_print_warning "Recursive defrag on directory: $target"
|
||||
df_confirm "Continue?" || return 0
|
||||
sudo btrfs filesystem defragment -r -v "$target"
|
||||
else
|
||||
echo -e "${DF_BLUE}==>${DF_NC} Defragmenting: $target"
|
||||
df_print_step "Defragmenting: $target"
|
||||
sudo btrfs filesystem defragment -v "$target"
|
||||
fi
|
||||
|
||||
echo -e "${DF_GREEN}✓${DF_NC} Defragmentation complete"
|
||||
df_print_success "Defragmentation complete"
|
||||
}
|
||||
|
||||
# Show compression stats (requires compsize)
|
||||
btrfs-compress() {
|
||||
_btrfs_check || return 1
|
||||
local target="${1:-$BTRFS_DEFAULT_MOUNT}"
|
||||
|
||||
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_require_cmd compsize || return 1
|
||||
df_print_func_name "Btrfs Compression Statistics"
|
||||
|
||||
sudo compsize "$target"
|
||||
sudo compsize "${1:-$BTRFS_DEFAULT_MOUNT}"
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Information Commands
|
||||
# ============================================================================
|
||||
|
||||
# Full filesystem info
|
||||
btrfs-info() {
|
||||
_btrfs_check || return 1
|
||||
local mount="${1:-$BTRFS_DEFAULT_MOUNT}"
|
||||
|
||||
df_print_func_name "Btrfs Filesystem Information"
|
||||
|
||||
echo -e "${DF_CYAN}Filesystem Show:${DF_NC}"
|
||||
df_print_section "Filesystem Show"
|
||||
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 ""
|
||||
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_check || return 1
|
||||
local mount="${1:-$BTRFS_DEFAULT_MOUNT}"
|
||||
|
||||
df_print_func_name "Btrfs Health Check"
|
||||
|
||||
local issues=0
|
||||
|
||||
# Check device stats for errors
|
||||
echo -e "${DF_CYAN}Device Errors:${DF_NC}"
|
||||
local stats=$(sudo btrfs device stats "$mount" 2>/dev/null)
|
||||
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 '%')
|
||||
df_print_section "Device Errors"
|
||||
local errors=$(sudo btrfs device stats "$mount" 2>/dev/null | grep -v " 0$" | grep -v "^$")
|
||||
[[ -z "$errors" ]] && df_print_indent "✓ No errors" || { df_print_indent "✗ Errors detected:"; echo "$errors" | sed 's/^/ /'; ((issues++)); }
|
||||
|
||||
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 (( used_pct >= 90 )); then
|
||||
echo -e " ${DF_RED}✗${DF_NC} Filesystem ${used_pct}% full - critical!"
|
||||
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"
|
||||
(( used_pct >= 90 )) && { df_print_indent "✗ ${used_pct}% full - critical!"; ((issues++)); } || \
|
||||
(( used_pct >= 80 )) && df_print_indent "⚠ ${used_pct}% full" || df_print_indent "✓ ${used_pct}% used"
|
||||
fi
|
||||
|
||||
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_check || return 1
|
||||
|
||||
df_print_func_name "Snapshot Disk Space Usage"
|
||||
|
||||
if [[ -d "/.snapshots" ]]; then
|
||||
echo -e "${DF_CYAN}Snapshot Directory:${DF_NC}"
|
||||
local size
|
||||
size=$(sudo du -sh /.snapshots 2>/dev/null | cut -f1)
|
||||
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"
|
||||
df_print_section "Snapshot Directory"
|
||||
local size=$(timeout 10 sudo du -sh /.snapshots 2>/dev/null | cut -f1)
|
||||
df_print_indent "${size:-Unable to calculate}"
|
||||
echo ""
|
||||
df_print_section "Individual Snapshots (top 10)"
|
||||
timeout 30 sudo du -sh /.snapshots/*/ 2>/dev/null | sort -h | tail -10 | sed 's/^/ /'
|
||||
else
|
||||
echo -e "${DF_YELLOW}⚠${DF_NC} No /.snapshots directory found"
|
||||
df_print_warning "No /.snapshots directory found"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Maintenance
|
||||
# ============================================================================
|
||||
|
||||
# Full maintenance routine
|
||||
btrfs-maintain() {
|
||||
_btrfs_check || return 1
|
||||
local mount="${1:-$BTRFS_DEFAULT_MOUNT}"
|
||||
|
||||
df_print_func_name "Btrfs Maintenance Routine"
|
||||
|
||||
echo "This will perform:"
|
||||
echo " 1. 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"
|
||||
echo "This will: health check, balance, scrub"
|
||||
df_confirm_warning "This may take several hours" || return 0
|
||||
df_print_step "Step 1/3: Health Check"
|
||||
btrfs-health "$mount"
|
||||
|
||||
echo -e "${DF_BLUE}==>${DF_NC} Step 2/3: Balance"
|
||||
df_print_step "Step 2/3: Balance"
|
||||
sudo btrfs balance start -dusage=50 -musage=50 "$mount"
|
||||
|
||||
echo ""
|
||||
echo -e "${DF_BLUE}==>${DF_NC} Step 3/3: Scrub"
|
||||
sudo btrfs scrub start -B "$mount" # -B runs in foreground
|
||||
|
||||
echo ""
|
||||
echo -e "${DF_GREEN}✓${DF_NC} Maintenance complete"
|
||||
btrfs-health "$mount"
|
||||
df_print_step "Step 3/3: Scrub"
|
||||
sudo btrfs scrub start -B "$mount"
|
||||
df_print_success "Maintenance complete"
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Aliases
|
||||
# ============================================================================
|
||||
|
||||
alias btru='btrfs-usage'
|
||||
alias btrs='btrfs-subs'
|
||||
alias btrh='btrfs-health'
|
||||
alias btri='btrfs-info'
|
||||
alias btrc='btrfs-compress'
|
||||
|
||||
# ============================================================================
|
||||
# Help
|
||||
# ============================================================================
|
||||
|
||||
btrfs-help() {
|
||||
df_print_func_name "Btrfs Helper Commands"
|
||||
|
||||
cat << 'EOF'
|
||||
|
||||
Information:
|
||||
btrfs-usage [mount] Filesystem usage summary
|
||||
btrfs-subs [mount] List all subvolumes
|
||||
btrfs-info [mount] Full filesystem information
|
||||
btrfs-health [mount] Quick health check
|
||||
btrfs-compress [path] Compression statistics (requires compsize)
|
||||
|
||||
Maintenance:
|
||||
btrfs-balance [mount] Start balance operation
|
||||
btrfs-balance-status Check balance progress
|
||||
btrfs-balance-cancel Cancel running balance
|
||||
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
|
||||
|
||||
btrfs-usage [mount] Filesystem usage
|
||||
btrfs-subs [mount] List subvolumes
|
||||
btrfs-info [mount] Full filesystem info
|
||||
btrfs-health [mount] Quick health check
|
||||
btrfs-compress [path] Compression stats
|
||||
btrfs-balance [mount] Start balance
|
||||
btrfs-scrub [mount] Start scrub
|
||||
btrfs-defrag <path> Defragment
|
||||
btrfs-snap-usage Snapshot space usage
|
||||
btrfs-maintain [mount] Full maintenance
|
||||
EOF
|
||||
}
|
||||
|
||||
alias btru='btrfs-usage' btrs='btrfs-subs' btrh='btrfs-health' btri='btrfs-info' btrc='btrfs-compress'
|
||||
|
||||
@@ -2,322 +2,135 @@
|
||||
# Command Palette - Fuzzy Command Launcher for Zsh
|
||||
# ============================================================================
|
||||
# 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)
|
||||
#
|
||||
# Requirements: fzf
|
||||
# ============================================================================
|
||||
|
||||
# Source shared colors (with fallback)
|
||||
source "${0:A:h}/../lib/colors.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'
|
||||
}
|
||||
source "${0:A:h}/../lib/utils.zsh" 2>/dev/null || \
|
||||
source "$HOME/.dotfiles/zsh/lib/utils.zsh" 2>/dev/null
|
||||
|
||||
# ============================================================================
|
||||
# Configuration
|
||||
# ============================================================================
|
||||
|
||||
typeset -g PALETTE_HOTKEY="${PALETTE_HOTKEY:-^@}" # Ctrl+Space
|
||||
typeset -g PALETTE_HOTKEY="${PALETTE_HOTKEY:-^@}"
|
||||
typeset -g PALETTE_HISTORY_SIZE=50
|
||||
typeset -g PALETTE_BOOKMARKS_FILE="$HOME/.dotfiles/.bookmarks"
|
||||
typeset -g DOTFILES_DIR="${DOTFILES_DIR:-$HOME/.dotfiles}"
|
||||
|
||||
# Icons (works with most terminals)
|
||||
typeset -g ICON_ALIAS="⚡"
|
||||
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
|
||||
# ============================================================================
|
||||
typeset -g ICON_ALIAS="⚡" ICON_FUNC="λ" ICON_HIST="↺" ICON_DIR="📁"
|
||||
typeset -g ICON_SCRIPT="⚙" ICON_ACTION="★" ICON_GIT="⎇"
|
||||
|
||||
_palette_get_aliases() {
|
||||
alias | sed 's/^alias //' | while IFS='=' read -r alias_name cmd; do
|
||||
cmd="${cmd#\'}"
|
||||
cmd="${cmd%\'}"
|
||||
cmd="${cmd#\"}"
|
||||
cmd="${cmd%\"}"
|
||||
printf "%s\t%s\t%s\t%s\n" "$ICON_ALIAS" "alias" "$alias_name" "$cmd"
|
||||
alias | sed 's/^alias //' | while IFS='=' read -r name cmd; do
|
||||
cmd="${cmd#\'}"; cmd="${cmd%\'}"; cmd="${cmd#\"}"; cmd="${cmd%\"}"
|
||||
printf "%s\t%s\t%s\t%s\n" "$ICON_ALIAS" "alias" "$name" "$cmd"
|
||||
done
|
||||
}
|
||||
|
||||
_palette_get_functions() {
|
||||
print -l ${(ok)functions} | grep -v '^_' | while read -r func_name; do
|
||||
printf "%s\t%s\t%s\t%s\n" "$ICON_FUNC" "func" "$func_name" "function"
|
||||
print -l ${(ok)functions} | grep -v "^_" | while read -r name; do
|
||||
printf "%s\t%s\t%s\t%s\n" "$ICON_FUNC" "function" "$name" ""
|
||||
done
|
||||
}
|
||||
|
||||
_palette_get_history() {
|
||||
fc -ln -$PALETTE_HISTORY_SIZE | tac | awk '!seen[$0]++' | head -30 | while read -r cmd; do
|
||||
[[ -n "$cmd" ]] && printf "%s\t%s\t%s\t%s\n" "$ICON_HIST" "history" "${cmd:0:50}" "$cmd"
|
||||
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" ""
|
||||
done
|
||||
}
|
||||
|
||||
_palette_get_bookmarks() {
|
||||
[[ ! -f "$PALETTE_BOOKMARKS_FILE" ]] && return
|
||||
|
||||
while IFS='|' read -r bm_name bm_path; do
|
||||
[[ -n "$bm_name" && -n "$bm_path" ]] && printf "%s\t%s\t%s\t%s\n" "$ICON_DIR" "bookmark" "$bm_name" "cd $bm_path"
|
||||
while IFS='|' read -r name path desc; do
|
||||
[[ "$name" =~ ^# || -z "$name" ]] && continue
|
||||
printf "%s\t%s\t%s\t%s\n" "$ICON_DIR" "bookmark" "$name" "$path"
|
||||
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() {
|
||||
printf "%s\t%s\t%s\t%s\n" "$ICON_ACTION" "action" "Reload shell" "exec zsh"
|
||||
printf "%s\t%s\t%s\t%s\n" "$ICON_EDIT" "action" "Edit .zshrc" "${EDITOR:-vim} ~/.zshrc"
|
||||
printf "%s\t%s\t%s\t%s\n" "$ICON_EDIT" "action" "Edit dotfiles.conf" "${EDITOR:-vim} $DOTFILES_DIR/dotfiles.conf"
|
||||
printf "%s\t%s\t%s\t%s\n" "$ICON_EDIT" "action" "Edit theme" "${EDITOR:-vim} $DOTFILES_DIR/zsh/themes/adlee.zsh-theme"
|
||||
printf "%s\t%s\t%s\t%s\n" "$ICON_SCRIPT" "action" "Dotfiles doctor" "dfd"
|
||||
printf "%s\t%s\t%s\t%s\n" "$ICON_SCRIPT" "action" "Dotfiles sync" "dfs"
|
||||
printf "%s\t%s\t%s\t%s\n" "$ICON_SCRIPT" "action" "Shell stats" "dfstats"
|
||||
printf "%s\t%s\t%s\t%s\n" "$ICON_SCRIPT" "action" "Compile zsh" "dfcompile"
|
||||
printf "%s\t%s\t%s\t%s\n" "$ICON_SCRIPT" "action" "Vault list" "vault list"
|
||||
printf "%s\t%s\t%s\t%s\n" "$ICON_ACTION" "action" "Clear screen" "clear"
|
||||
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 ~"
|
||||
cat << 'EOF'
|
||||
★ action reload-shell Reload zsh configuration
|
||||
★ action edit-zshrc Edit ~/.zshrc
|
||||
★ action dotfiles-update Update dotfiles
|
||||
EOF
|
||||
df_in_git_repo && cat << 'EOF'
|
||||
⎇ git git-status Show git status
|
||||
⎇ git git-pull Pull latest
|
||||
⎇ git git-push Push commits
|
||||
EOF
|
||||
}
|
||||
|
||||
_palette_get_directories() {
|
||||
dirs -v 2>/dev/null | tail -n +2 | head -10 | while read -r num dir; do
|
||||
[[ -n "$dir" ]] && printf "%s\t%s\t%s\t%s\n" "$ICON_DIR" "recent" "$dir" "cd $dir"
|
||||
done
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Main Palette Function
|
||||
# ============================================================================
|
||||
|
||||
_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"
|
||||
;;
|
||||
_palette_run_action() {
|
||||
case "$1" in
|
||||
reload-shell) source ~/.zshrc; df_print_success "Shell reloaded" ;;
|
||||
edit-zshrc) ${EDITOR:-vim} ~/.zshrc ;;
|
||||
dotfiles-update) cd "$DOTFILES_DIR" && git pull ;;
|
||||
git-status) git status ;;
|
||||
git-pull) git pull ;;
|
||||
git-push) git push ;;
|
||||
*) df_print_error "Unknown action: $1" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Alias for easier access
|
||||
palette() { command_palette; }
|
||||
p() { command_palette; }
|
||||
|
||||
# ============================================================================
|
||||
# Bookmark Management
|
||||
# ============================================================================
|
||||
palette() {
|
||||
df_require_cmd fzf || return 1
|
||||
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
|
||||
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() {
|
||||
local bm_name="$1"
|
||||
local bm_path="${2:-$(pwd)}"
|
||||
|
||||
# Ensure bookmarks file parent directory exists
|
||||
mkdir -p "$(dirname "$PALETTE_BOOKMARKS_FILE")" 2>/dev/null
|
||||
|
||||
# Create bookmarks file if it doesn't exist
|
||||
[[ ! -f "$PALETTE_BOOKMARKS_FILE" ]] && touch "$PALETTE_BOOKMARKS_FILE"
|
||||
|
||||
if [[ -z "$bm_name" ]]; then
|
||||
echo "Usage: bookmark <name> [path]"
|
||||
echo " bookmark list"
|
||||
echo " bookmark delete <name>"
|
||||
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
|
||||
local cmd="${1:-list}"; shift 2>/dev/null
|
||||
case "$cmd" in
|
||||
add)
|
||||
local name="$1" path="${2:-$(pwd)}" desc="$3"
|
||||
[[ -z "$name" ]] && { echo "Usage: bookmark add <name> [path]"; return 1; }
|
||||
df_ensure_file "$PALETTE_BOOKMARKS_FILE" "# Bookmarks: name|path|description"
|
||||
grep -q "^${name}|" "$PALETTE_BOOKMARKS_FILE" 2>/dev/null && {
|
||||
df_confirm "Overwrite '$name'?" || return 1
|
||||
grep -v "^${name}|" "$PALETTE_BOOKMARKS_FILE" > "${PALETTE_BOOKMARKS_FILE}.tmp"
|
||||
mv "${PALETTE_BOOKMARKS_FILE}.tmp" "$PALETTE_BOOKMARKS_FILE"
|
||||
}
|
||||
echo "${name}|${path}|${desc}" >> "$PALETTE_BOOKMARKS_FILE"
|
||||
df_print_success "Bookmarked: $name → $path"
|
||||
;;
|
||||
delete|rm)
|
||||
local to_delete="$2"
|
||||
if [[ -z "$to_delete" ]]; then
|
||||
echo "Specify bookmark to delete"
|
||||
return 1
|
||||
fi
|
||||
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
|
||||
[[ -z "$1" ]] && { echo "Usage: bookmark delete <name>"; return 1; }
|
||||
grep -q "^${1}|" "$PALETTE_BOOKMARKS_FILE" 2>/dev/null || { df_print_error "Not found: $1"; return 1; }
|
||||
grep -v "^${1}|" "$PALETTE_BOOKMARKS_FILE" > "${PALETTE_BOOKMARKS_FILE}.tmp"
|
||||
mv "${PALETTE_BOOKMARKS_FILE}.tmp" "$PALETTE_BOOKMARKS_FILE"
|
||||
df_print_success "Deleted: $1"
|
||||
;;
|
||||
*)
|
||||
# Remove existing bookmark with same name (if file has content)
|
||||
if [[ -s "$PALETTE_BOOKMARKS_FILE" ]]; then
|
||||
local temp_file="${PALETTE_BOOKMARKS_FILE}.tmp.$$"
|
||||
grep -v "^${bm_name}|" "$PALETTE_BOOKMARKS_FILE" > "$temp_file" 2>/dev/null || true
|
||||
mv -f "$temp_file" "$PALETTE_BOOKMARKS_FILE"
|
||||
fi
|
||||
# Add new bookmark
|
||||
echo "${bm_name}|${bm_path}" >> "$PALETTE_BOOKMARKS_FILE"
|
||||
echo -e "${DF_GREEN}✓${DF_NC} Bookmarked: $bm_name → $bm_path"
|
||||
list|ls)
|
||||
df_print_func_name "Bookmarks"
|
||||
[[ ! -f "$PALETTE_BOOKMARKS_FILE" ]] && { df_print_info "No bookmarks"; return; }
|
||||
while IFS='|' read -r name path desc; do
|
||||
[[ "$name" =~ ^# || -z "$name" ]] && continue
|
||||
df_print_indent "● $name → $path"
|
||||
done < "$PALETTE_BOOKMARKS_FILE"
|
||||
;;
|
||||
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
|
||||
}
|
||||
|
||||
# Quick jump to bookmark
|
||||
jump() {
|
||||
local bm_name="$1"
|
||||
|
||||
if [[ -z "$bm_name" ]]; then
|
||||
# Fuzzy select bookmark
|
||||
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
|
||||
bm() {
|
||||
df_require_cmd fzf || return 1
|
||||
[[ ! -f "$PALETTE_BOOKMARKS_FILE" ]] && { df_print_info "No bookmarks"; return 1; }
|
||||
local sel=$(grep -v "^#" "$PALETTE_BOOKMARKS_FILE" | grep -v "^$" | \
|
||||
fzf $(df_fzf_opts) --delimiter='|' --with-nth=1,2 --prompt='Bookmark > ')
|
||||
[[ -n "$sel" ]] && cd "$(echo "$sel" | cut -d'|' -f2)"
|
||||
}
|
||||
|
||||
# Aliases
|
||||
bm() { bookmark "$@"; }
|
||||
j() { jump "$@"; }
|
||||
|
||||
# ============================================================================
|
||||
# Widget for Keybinding
|
||||
# ============================================================================
|
||||
|
||||
_palette_widget() {
|
||||
command_palette
|
||||
zle reset-prompt
|
||||
}
|
||||
|
||||
# Register widget
|
||||
_palette_widget() { BUFFER=""; zle redisplay; palette; zle reset-prompt; }
|
||||
zle -N _palette_widget
|
||||
|
||||
# Bind to Ctrl+Space (^@)
|
||||
bindkey "$PALETTE_HOTKEY" _palette_widget
|
||||
|
||||
# Alternative binding: Ctrl+P
|
||||
bindkey '^P' _palette_widget
|
||||
alias p='palette' bml='bookmark list' bma='bookmark add' bmg='bookmark go'
|
||||
|
||||
@@ -1,198 +1,84 @@
|
||||
# ============================================================================
|
||||
# Password Manager Integration for Zsh (LastPass Only)
|
||||
# ============================================================================
|
||||
# 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
|
||||
# Password Manager Integration (LastPass CLI)
|
||||
# ============================================================================
|
||||
|
||||
# Source shared colors (with fallback)
|
||||
source "${0:A:h}/../lib/colors.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'
|
||||
}
|
||||
source "${0:A:h}/../lib/utils.zsh" 2>/dev/null || \
|
||||
source "$HOME/.dotfiles/zsh/lib/utils.zsh" 2>/dev/null
|
||||
|
||||
# ============================================================================
|
||||
# LastPass Functions
|
||||
# ============================================================================
|
||||
typeset -g PW_CLIP_TIME="${PW_CLIP_TIME:-45}"
|
||||
|
||||
_lp_ensure_session() {
|
||||
_pw_check() {
|
||||
df_require_cmd lpass lastpass-cli || return 1
|
||||
if ! lpass status -q 2>/dev/null; then
|
||||
echo "Signing into LastPass..." >&2
|
||||
lpass login "${LASTPASS_EMAIL:-}"
|
||||
df_print_warning "Not logged in"
|
||||
df_print_step "Logging in..."
|
||||
lpass login --trust "${LPASS_USER:-}" || { df_print_error "Login failed"; return 1; }
|
||||
fi
|
||||
}
|
||||
|
||||
_lp_list() {
|
||||
_lp_ensure_session
|
||||
lpass ls --format="%an\t%ag" 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() {
|
||||
local cmd="${1:-help}"
|
||||
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"
|
||||
_pw_copy() {
|
||||
local text="$1" label="${2:-Password}"
|
||||
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; }) &
|
||||
}
|
||||
|
||||
pw() {
|
||||
local cmd="${1:-search}"
|
||||
case "$cmd" in
|
||||
list|ls|l)
|
||||
df_print_func_name "LastPass Vault"
|
||||
_lp_list
|
||||
login) lpass login --trust "${LPASS_USER:-}" ;;
|
||||
logout) lpass logout -f; df_print_success "Logged out" ;;
|
||||
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"
|
||||
;;
|
||||
|
||||
get|g|show)
|
||||
local item="$1"
|
||||
local field="${2:-password}"
|
||||
[[ -z "$item" ]] && { echo "Usage: pw get <item> [field]"; return 1; }
|
||||
_lp_get "$item" "$field"
|
||||
;;
|
||||
|
||||
otp|totp|2fa)
|
||||
local item="$1"
|
||||
[[ -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"
|
||||
list|ls) _pw_check || return 1; df_print_func_name "Password Entries"; lpass ls --long ;;
|
||||
search|*)
|
||||
_pw_check || return 1
|
||||
local query="$1"; [[ "$cmd" == "search" ]] && query="$2"
|
||||
if df_cmd_exists fzf && [[ -z "$query" ]]; then
|
||||
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[^\]]+(?=\]$)')
|
||||
local pass=$(lpass show --password "$id" 2>/dev/null)
|
||||
[[ -n "$pass" ]] && _pw_copy "$pass" || df_print_error "Could not retrieve"
|
||||
else
|
||||
echo -e "${DF_RED}✗${DF_NC} Item not found or empty"
|
||||
return 1
|
||||
[[ -z "$query" ]] && { echo "Usage: pw <search-term>"; 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
|
||||
;;
|
||||
|
||||
lock)
|
||||
lpass logout -f 2>/dev/null
|
||||
echo -e "${DF_GREEN}✓${DF_NC} Logged out of LastPass"
|
||||
;;
|
||||
|
||||
help|--help|-h|*)
|
||||
df_print_func_name "Password Manager CLI"
|
||||
echo "Usage: pw <command> [args]"
|
||||
echo
|
||||
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"
|
||||
help|--help|-h)
|
||||
df_print_func_name "Password Manager"
|
||||
cat << 'EOF'
|
||||
pw <search> Search and copy password
|
||||
pw show <n> Show entry details
|
||||
pw list List all entries
|
||||
pw gen [len] Generate password (default: 20)
|
||||
pw sync Sync vault
|
||||
pw login/logout Auth commands
|
||||
EOF
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# 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
|
||||
alias pwc='pw' pws='pw show' pwg='pw gen' pwl='pw list'
|
||||
|
||||
@@ -1,488 +1,173 @@
|
||||
# ============================================================================
|
||||
# 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/colors.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'
|
||||
}
|
||||
source "${0:A:h}/../lib/utils.zsh" 2>/dev/null || \
|
||||
source "$HOME/.dotfiles/zsh/lib/utils.zsh" 2>/dev/null
|
||||
|
||||
# ============================================================================
|
||||
# Configuration
|
||||
# ============================================================================
|
||||
typeset -g PY_PYTHON="${PY_PYTHON:-python3}"
|
||||
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}"
|
||||
typeset -g PY_TEMPLATE_PYTHON="${PY_TEMPLATE_PYTHON:-python3}"
|
||||
typeset -g PY_TEMPLATE_VENV_NAME="${PY_TEMPLATE_VENV_NAME:-venv}"
|
||||
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
|
||||
_py_check_name() {
|
||||
[[ -z "$1" ]] && { df_print_warning "Project name required"; return 1; }
|
||||
[[ -d "$1" ]] && { df_print_warning "Directory '$1' exists"; return 1; }
|
||||
return 0
|
||||
}
|
||||
|
||||
_py_create_venv() {
|
||||
local project_dir="$1"
|
||||
_py_print_step "Creating virtual environment"
|
||||
|
||||
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_venv() {
|
||||
df_print_step "Creating virtual environment"
|
||||
"$PY_PYTHON" -m venv "$1/$PY_VENV"
|
||||
df_print_success "Created: $PY_VENV"
|
||||
}
|
||||
|
||||
_py_create_gitignore() {
|
||||
local project_dir="$1"
|
||||
cat > "$project_dir/.gitignore" << 'EOF'
|
||||
_py_gitignore() {
|
||||
cat > "$1/.gitignore" << 'EOF'
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
dist/
|
||||
*.egg-info/
|
||||
venv/
|
||||
env/
|
||||
.venv
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
.venv/
|
||||
.env
|
||||
*.log
|
||||
*.db
|
||||
*.sqlite3
|
||||
.pytest_cache/
|
||||
.mypy_cache/
|
||||
EOF
|
||||
_py_print_success "Created .gitignore"
|
||||
df_print_success "Created .gitignore"
|
||||
}
|
||||
|
||||
_py_init_git() {
|
||||
local project_dir="$1"
|
||||
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_git() {
|
||||
[[ "$PY_GIT_INIT" == "true" ]] && { cd "$1"; git init; git add .; git commit -m "Initial commit"; df_print_success "Git initialized"; }
|
||||
}
|
||||
|
||||
_py_show_next_steps() {
|
||||
local project_name="$1"
|
||||
local has_venv="$2"
|
||||
echo
|
||||
echo -e "${DF_CYAN}Next steps:${DF_NC}"
|
||||
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
|
||||
_py_next() {
|
||||
echo ""
|
||||
df_print_section "Next steps"
|
||||
df_print_indent "cd $1"
|
||||
df_print_indent "source $PY_VENV/bin/activate"
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Base Python Project Template
|
||||
# ============================================================================
|
||||
|
||||
py-new() {
|
||||
local project_name="$1"
|
||||
_py_check_project_name "$project_name" || return 1
|
||||
|
||||
df_print_func_name "Python Project: $project_name"
|
||||
|
||||
_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'
|
||||
_py_check_name "$1" || return 1
|
||||
df_print_func_name "Python Project: $1"
|
||||
mkdir -p "$1"/{src,tests}
|
||||
touch "$1/src/__init__.py" "$1/tests/__init__.py"
|
||||
cat > "$1/src/main.py" << 'EOF'
|
||||
#!/usr/bin/env python3
|
||||
"""Main module."""
|
||||
|
||||
def main():
|
||||
print("Hello from Python!")
|
||||
print("Hello!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
EOF
|
||||
|
||||
cat > "$project_name/requirements.txt" << 'EOF'
|
||||
# Production dependencies
|
||||
# Development: pytest, black, flake8, mypy
|
||||
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"
|
||||
echo "# Dependencies" > "$1/requirements.txt"
|
||||
_py_venv "$1"; _py_gitignore "$1"; _py_git "$1"
|
||||
df_print_success "Created: $1"
|
||||
_py_next "$1"
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# 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() {
|
||||
local project_name="$1"
|
||||
_py_check_project_name "$project_name" || return 1
|
||||
|
||||
df_print_func_name "Flask Project: $project_name"
|
||||
|
||||
mkdir -p "$project_name"/{app/{templates,static/{css,js}},tests}
|
||||
_py_create_venv "$project_name"
|
||||
|
||||
cd "$project_name"
|
||||
_py_print_step "Installing Flask"
|
||||
"$PY_TEMPLATE_VENV_NAME/bin/pip" install flask
|
||||
|
||||
cat > "app/__init__.py" << 'EOF'
|
||||
_py_check_name "$1" || return 1
|
||||
df_print_func_name "Flask Project: $1"
|
||||
mkdir -p "$1"/{app/{templates,static},tests}
|
||||
_py_venv "$1"
|
||||
df_print_step "Installing Flask"
|
||||
"$1/$PY_VENV/bin/pip" install flask -q
|
||||
cat > "$1/app/__init__.py" << 'EOF'
|
||||
from flask import Flask
|
||||
|
||||
def create_app(config=None):
|
||||
def create_app():
|
||||
app = Flask(__name__)
|
||||
if config:
|
||||
app.config.from_object(config)
|
||||
from app.routes import main
|
||||
app.register_blueprint(main)
|
||||
return app
|
||||
EOF
|
||||
|
||||
cat > "app/routes.py" << 'EOF'
|
||||
cat > "$1/app/routes.py" << 'EOF'
|
||||
from flask import Blueprint, render_template
|
||||
|
||||
main = Blueprint('main', __name__)
|
||||
|
||||
@main.route('/')
|
||||
def index():
|
||||
return render_template('index.html')
|
||||
EOF
|
||||
|
||||
cat > "app.py" << 'EOF'
|
||||
echo '<!DOCTYPE html><html><body><h1>Flask</h1></body></html>' > "$1/app/templates/index.html"
|
||||
cat > "$1/app.py" << 'EOF'
|
||||
#!/usr/bin/env python3
|
||||
from app import create_app
|
||||
|
||||
app = create_app()
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True, host='0.0.0.0', port=5000)
|
||||
app.run(debug=True)
|
||||
EOF
|
||||
chmod +x app.py
|
||||
|
||||
cat > "app/templates/index.html" << 'EOF'
|
||||
<!DOCTYPE html>
|
||||
<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"
|
||||
echo "Flask>=3.0.0" > "$1/requirements.txt"
|
||||
_py_gitignore "$1"; _py_git "$1"
|
||||
df_print_success "Created: $1"
|
||||
_py_next "$1"
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# FastAPI Project Template
|
||||
# ============================================================================
|
||||
|
||||
py-fastapi() {
|
||||
local project_name="$1"
|
||||
_py_check_project_name "$project_name" || return 1
|
||||
|
||||
df_print_func_name "FastAPI Project: $project_name"
|
||||
|
||||
mkdir -p "$project_name"/{app/{api,models,schemas},tests}
|
||||
_py_create_venv "$project_name"
|
||||
|
||||
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'
|
||||
_py_check_name "$1" || return 1
|
||||
df_print_func_name "FastAPI Project: $1"
|
||||
mkdir -p "$1"/{app,tests}
|
||||
_py_venv "$1"
|
||||
df_print_step "Installing FastAPI"
|
||||
"$1/$PY_VENV/bin/pip" install fastapi uvicorn -q
|
||||
cat > "$1/app/main.py" << 'EOF'
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
app = FastAPI(title="My API", version="0.1.0")
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
app = FastAPI()
|
||||
@app.get("/")
|
||||
def root():
|
||||
return {"message": "Welcome to FastAPI"}
|
||||
|
||||
return {"message": "Hello"}
|
||||
@app.get("/health")
|
||||
def health():
|
||||
return {"status": "healthy"}
|
||||
return {"status": "ok"}
|
||||
EOF
|
||||
|
||||
cat > "run.py" << 'EOF'
|
||||
cat > "$1/run.py" << 'EOF'
|
||||
#!/usr/bin/env python3
|
||||
import uvicorn
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run("app.main:app", host="0.0.0.0", port=8000, reload=True)
|
||||
EOF
|
||||
chmod +x run.py
|
||||
|
||||
cat > "requirements.txt" << 'EOF'
|
||||
fastapi>=0.104.0
|
||||
uvicorn[standard]>=0.24.0
|
||||
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"
|
||||
echo -e "fastapi>=0.104.0\nuvicorn>=0.24.0" > "$1/requirements.txt"
|
||||
_py_gitignore "$1"; _py_git "$1"
|
||||
df_print_success "Created: $1"
|
||||
df_print_info "Docs: http://localhost:8000/docs"
|
||||
_py_next "$1"
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# 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() {
|
||||
local project_name="$1"
|
||||
_py_check_project_name "$project_name" || return 1
|
||||
|
||||
df_print_func_name "CLI Tool Project: $project_name"
|
||||
|
||||
mkdir -p "$project_name"/{src/$project_name,tests}
|
||||
_py_create_venv "$project_name"
|
||||
|
||||
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'
|
||||
_py_check_name "$1" || return 1
|
||||
df_print_func_name "CLI Project: $1"
|
||||
mkdir -p "$1"/{src/$1,tests}
|
||||
_py_venv "$1"
|
||||
df_print_step "Installing click"
|
||||
"$1/$PY_VENV/bin/pip" install click -q
|
||||
echo '__version__ = "0.1.0"' > "$1/src/$1/__init__.py"
|
||||
cat > "$1/src/$1/cli.py" << 'EOF'
|
||||
#!/usr/bin/env python3
|
||||
import click
|
||||
|
||||
@click.group()
|
||||
@click.version_option()
|
||||
def cli():
|
||||
"""CLI tool - A command-line utility."""
|
||||
pass
|
||||
|
||||
@cli.command()
|
||||
@click.argument('name', default='World')
|
||||
def greet(name):
|
||||
"""Greet someone."""
|
||||
click.echo(f"Hello, {name}!")
|
||||
|
||||
if __name__ == '__main__':
|
||||
cli()
|
||||
EOF
|
||||
|
||||
cat > "setup.py" << EOF
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
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"
|
||||
echo "click>=8.1.0" > "$1/requirements.txt"
|
||||
_py_gitignore "$1"; _py_git "$1"
|
||||
df_print_success "Created: $1"
|
||||
df_print_info "Install: pip install -e $1"
|
||||
_py_next "$1"
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# 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() {
|
||||
if [[ -d "venv" ]]; then
|
||||
source venv/bin/activate
|
||||
elif [[ -d ".venv" ]]; then
|
||||
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
|
||||
[[ -d "venv" ]] && source venv/bin/activate && return
|
||||
[[ -d ".venv" ]] && source .venv/bin/activate && return
|
||||
df_print_error "No venv found"
|
||||
}
|
||||
|
||||
alias pynew='py-new' pyflask='py-flask' pyfast='py-fastapi' pycli='py-cli'
|
||||
|
||||
@@ -1,291 +1,82 @@
|
||||
# ============================================================================
|
||||
# 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/colors.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
|
||||
# ============================================================================
|
||||
source "${0:A:h}/../lib/utils.zsh" 2>/dev/null || \
|
||||
source "$HOME/.dotfiles/zsh/lib/utils.zsh" 2>/dev/null
|
||||
|
||||
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"
|
||||
|
||||
# ============================================================================
|
||||
# Common Typo Database
|
||||
# ============================================================================
|
||||
|
||||
typeset -gA TYPO_CORRECTIONS=(
|
||||
# Git typos
|
||||
[gti]="git" [gitt]="git" [got]="git" [gut]="git" [gi]="git"
|
||||
[giit]="git" [ggit]="git" [gitst]="git st" [gits]="git s"
|
||||
[gitl]="git l" [gitd]="git d" [gitp]="git p"
|
||||
[psuh]="push" [psull]="pull" [pul]="pull" [puhs]="push"
|
||||
[stauts]="status" [statis]="status" [statuus]="status"
|
||||
[comit]="commit" [commti]="commit" [commt]="commit"
|
||||
[chekcout]="checkout" [chekout]="checkout" [checkou]="checkout"
|
||||
[branhc]="branch" [barnch]="branch" [bracnh]="branch"
|
||||
[marge]="merge" [merg]="merge" [stsh]="stash" [stahs]="stash"
|
||||
|
||||
# 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"
|
||||
[gti]="git" [gitt]="git" [got]="git" [gi]="git"
|
||||
[gitst]="git st" [gits]="git s" [gitp]="git p"
|
||||
[psuh]="push" [psull]="pull" [pul]="pull"
|
||||
[stauts]="status" [comit]="commit" [commti]="commit"
|
||||
[chekcout]="checkout" [branhc]="branch" [marge]="merge"
|
||||
[dokcer]="docker" [doker]="docker" [dcoker]="docker"
|
||||
[sl]="ls" [sls]="ls" [cta]="cat" [grpe]="grep" [gerp]="grep"
|
||||
[mkdri]="mkdir" [chmdo]="chmod" [suod]="sudo" [sduo]="sudo"
|
||||
[pytohn]="python" [pyhton]="python" [ndoe]="node"
|
||||
[vmi]="vim" [cde]="code" [clera]="clear" [exti]="exit"
|
||||
)
|
||||
|
||||
# ============================================================================
|
||||
# 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=(
|
||||
[htop]="htop" [tree]="tree" [jq]="jq"
|
||||
[fd]="fd-find:apt fd:pacman fd:brew" [rg]="ripgrep"
|
||||
[bat]="bat" [eza]="eza" [exa]="exa" [fzf]="fzf"
|
||||
[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"
|
||||
[htop]="htop" [tree]="tree" [jq]="jq" [fd]="fd" [rg]="ripgrep"
|
||||
[bat]="bat" [eza]="eza" [fzf]="fzf" [tldr]="tldr" [ncdu]="ncdu"
|
||||
[lazygit]="lazygit" [neofetch]="neofetch" [delta]="git-delta"
|
||||
)
|
||||
|
||||
_ss_suggest_package() {
|
||||
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
|
||||
|
||||
_ss_track() {
|
||||
local cmd="$1"
|
||||
[[ ${#cmd} -lt 8 ]] && return
|
||||
|
||||
mkdir -p "$(dirname "$SMART_SUGGEST_TRACK_FILE")"
|
||||
df_ensure_dir "$(dirname "$SMART_SUGGEST_TRACK_FILE")"
|
||||
echo "$cmd" >> "$SMART_SUGGEST_TRACK_FILE"
|
||||
|
||||
local count=$(grep -Fc "$cmd" "$SMART_SUGGEST_TRACK_FILE" 2>/dev/null || echo 0)
|
||||
|
||||
if [[ $count -ge 10 && $((count % 10)) -eq 0 ]]; then
|
||||
_ss_suggest_alias_for "$cmd" "$count"
|
||||
if (( count >= 10 && count % 10 == 0 )); then
|
||||
local existing=$(alias | grep -F "='$cmd'" | head -1 | cut -d= -f1)
|
||||
[[ -n "$existing" ]] && df_print_info "You have alias: $existing" || \
|
||||
df_print_info "Consider: alias xyz='$cmd'"
|
||||
fi
|
||||
}
|
||||
|
||||
_ss_suggest_alias_for() {
|
||||
local cmd="$1"
|
||||
local count="$2"
|
||||
|
||||
local existing=$(alias | grep -F "='$cmd'" | head -1 | cut -d= -f1)
|
||||
|
||||
if [[ -n "$existing" ]]; then
|
||||
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
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Command Not Found Handler
|
||||
# ============================================================================
|
||||
|
||||
command_not_found_handler() {
|
||||
local cmd="$1"
|
||||
shift
|
||||
local args="$@"
|
||||
local cmd="$1"; shift
|
||||
[[ "$SMART_SUGGEST_ENABLED" != true ]] && { echo "zsh: command not found: $cmd"; return 127; }
|
||||
|
||||
[[ "$SMART_SUGGEST_ENABLED" != true ]] && {
|
||||
echo "zsh: command not found: $cmd"
|
||||
return 127
|
||||
}
|
||||
df_print_error "Command not found: $cmd"
|
||||
|
||||
echo -e "${DF_RED}✗${DF_NC} Command not found: ${DF_YELLOW}$cmd${DF_NC}"
|
||||
local correction="${TYPO_CORRECTIONS[$cmd]}"
|
||||
[[ -n "$correction" ]] && { df_print_info "Did you mean: $correction?"; df_print_indent "Run: $correction $@"; }
|
||||
|
||||
local suggestion_made=false
|
||||
|
||||
if [[ "$SMART_SUGGEST_TYPOS" == true ]]; then
|
||||
local correction="${TYPO_CORRECTIONS[$cmd]}"
|
||||
if [[ -n "$correction" ]]; then
|
||||
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 similar=$(compgen -c 2>/dev/null | grep -i "^${cmd:0:3}" | head -3 | tr '\n' ', ' | sed 's/,$//')
|
||||
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
|
||||
local pkg="${COMMAND_PACKAGES[$cmd]}"
|
||||
[[ -n "$pkg" ]] && df_print_info "Install: sudo pacman -S $pkg"
|
||||
|
||||
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() {
|
||||
local last_cmd=$(fc -ln -1 2>/dev/null | sed 's/^[[:space:]]*//')
|
||||
local first_word="${last_cmd%% *}"
|
||||
|
||||
local correction="${TYPO_CORRECTIONS[$first_word]}"
|
||||
|
||||
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
|
||||
local last=$(fc -ln -1 2>/dev/null | sed 's/^[[:space:]]*//')
|
||||
local first="${last%% *}"
|
||||
local fix="${TYPO_CORRECTIONS[$first]}"
|
||||
[[ -n "$fix" ]] && { df_print_step "Running: ${last/$first/$fix}"; eval "${last/$first/$fix}"; } || df_print_warning "No fix for: $last"
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Setup Hooks
|
||||
# ============================================================================
|
||||
_ss_preexec() { _ss_track "$1"; }
|
||||
_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() {
|
||||
autoload -Uz add-zsh-hook
|
||||
add-zsh-hook preexec _ss_preexec_hook
|
||||
add-zsh-hook precmd _ss_precmd_hook
|
||||
add-zsh-hook preexec _ss_preexec
|
||||
add-zsh-hook precmd _ss_precmd
|
||||
}
|
||||
|
||||
[[ "$SMART_SUGGEST_ENABLED" == true ]] && _ss_setup
|
||||
|
||||
@@ -2,253 +2,97 @@
|
||||
# Snapper Snapshot Functions for CachyOS/Arch with limine-snapper-sync
|
||||
# ============================================================================
|
||||
|
||||
# Source shared colors (with fallback)
|
||||
source "${0:A:h}/../lib/colors.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
|
||||
# ============================================================================
|
||||
source "${0:A:h}/../lib/utils.zsh" 2>/dev/null || \
|
||||
source "$HOME/.dotfiles/zsh/lib/utils.zsh" 2>/dev/null
|
||||
|
||||
snap-create() {
|
||||
local description="$*"
|
||||
local snap_config="root"
|
||||
local limine_conf="/boot/limine.conf"
|
||||
local desc="$*"
|
||||
local limine="/boot/limine.conf"
|
||||
|
||||
df_print_func_name "Snapper Snapshot Creation"
|
||||
|
||||
if [[ -z "$description" ]]; then
|
||||
echo -e "${DF_YELLOW}⚠${DF_NC} No description provided"
|
||||
echo -n "Enter snapshot description: "
|
||||
read description
|
||||
[[ -z "$description" ]] && { echo -e "${DF_RED}✗${DF_NC} Description required. Aborting."; return 1; }
|
||||
if [[ -z "$desc" ]]; then
|
||||
df_print_warning "No description"
|
||||
echo -n "Description: "; read desc
|
||||
[[ -z "$desc" ]] && { df_print_error "Required"; return 1; }
|
||||
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"
|
||||
local before_checksum=$(sudo md5sum "$limine_conf" | awk '{print $1}')
|
||||
local before_entries=$(sudo grep -cP "^\\s*///\\d+\\s*│" "$limine_conf" || echo "0")
|
||||
df_print_step "Checking limine.conf before snapshot"
|
||||
local before=$(sudo grep -cP "^\\s*///\\d+\\s*│" "$limine" || echo "0")
|
||||
df_print_success "Before: $before entries"
|
||||
|
||||
echo -e "${DF_GREEN}✓${DF_NC} Before: $before_entries snapshot entries"
|
||||
echo -e "${DF_GREEN}✓${DF_NC} Before checksum: $before_checksum"
|
||||
df_print_step "Creating snapshot: \"$desc\""
|
||||
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\""
|
||||
|
||||
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..."
|
||||
df_print_step "Triggering limine-snapper-sync..."
|
||||
sudo systemctl start limine-snapper-sync.service && df_print_success "Triggered" || df_print_warning "May run automatically"
|
||||
sleep 2
|
||||
|
||||
echo -e "${DF_BLUE}==>${DF_NC} Validating limine.conf update"
|
||||
local after_checksum=$(sudo md5sum "$limine_conf" | awk '{print $1}')
|
||||
local after_entries=$(sudo grep -cP "^\\s*///\\d+\\s*│" "$limine_conf" || echo "0")
|
||||
df_print_step "Validating"
|
||||
local after=$(sudo grep -cP "^\\s*///\\d+\\s*│" "$limine" || echo "0")
|
||||
|
||||
local validation_passed=true
|
||||
|
||||
if [[ "$before_checksum" == "$after_checksum" ]]; then
|
||||
echo -e "${DF_RED}✗${DF_NC} limine.conf was NOT updated (checksum unchanged)"
|
||||
validation_passed=false
|
||||
if sudo grep -qP "^\\s*///$num\\s*│" "$limine"; then
|
||||
df_print_success "Snapshot #$num in limine.conf"
|
||||
(( after > before )) && df_print_success "Added $((after - before)) entry"
|
||||
else
|
||||
echo -e "${DF_GREEN}✓${DF_NC} limine.conf was updated"
|
||||
fi
|
||||
|
||||
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
|
||||
df_print_error "Snapshot #$num NOT in limine.conf"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${DF_CYAN}Summary:${DF_NC}"
|
||||
echo -e " Snapshot Number: #$snapshot_num"
|
||||
echo -e " Description: \"$description\""
|
||||
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
|
||||
df_print_section "Summary"
|
||||
df_print_indent "Number: #$num"
|
||||
df_print_indent "Description: $desc"
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Helper Functions
|
||||
# ============================================================================
|
||||
|
||||
snap-list() {
|
||||
local count="${1:-10}"
|
||||
|
||||
df_print_func_name "Snapper Snapshots (last $count)"
|
||||
|
||||
sudo snapper -c root list | tail -n "$((count + 1))"
|
||||
}
|
||||
|
||||
snap-show() {
|
||||
[[ -z "$1" ]] && { echo -e "${DF_RED}✗${DF_NC} Usage: snap-show <snapshot_number>"; return 1; }
|
||||
|
||||
df_print_func_name "Snapshot #$1 Details"
|
||||
|
||||
[[ -z "$1" ]] && { echo "Usage: snap-show <num>"; return 1; }
|
||||
df_print_func_name "Snapshot #$1"
|
||||
sudo snapper -c root list | grep "^\s*$1\s"
|
||||
|
||||
echo -e "\n${DF_CYAN}In limine.conf:${DF_NC}"
|
||||
if sudo grep -qP "^\\s*///$1\\s*│" /boot/limine.conf; then
|
||||
local entry_line=$(sudo grep -nP "^\\s*///$1\\s*│" /boot/limine.conf | head -n 1 | cut -d: -f1)
|
||||
[[ -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
|
||||
echo ""
|
||||
df_print_section "In limine.conf"
|
||||
sudo grep -qP "^\\s*///$1\\s*│" /boot/limine.conf && \
|
||||
sudo grep -P "^\\s*///$1\\s*│" /boot/limine.conf || df_print_warning "Not found"
|
||||
}
|
||||
|
||||
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 limine_conf="/boot/limine.conf"
|
||||
local before=$(sudo grep -cP "^\\s*///\\d+\\s*│" /boot/limine.conf || echo "0")
|
||||
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 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"
|
||||
sudo grep -qP "^\\s*///$1\\s*│" /boot/limine.conf && df_print_error "Still in limine!" || df_print_success "Removed from limine"
|
||||
}
|
||||
|
||||
snap-sync() {
|
||||
df_print_func_name "Limine-Snapper-Sync"
|
||||
|
||||
echo -e "${DF_BLUE}==>${DF_NC} Manually triggering limine-snapper-sync..."
|
||||
|
||||
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
|
||||
df_print_step "Triggering sync..."
|
||||
sudo systemctl start limine-snapper-sync.service && { sleep 2; df_print_success "Done"; } || df_print_error "Failed"
|
||||
}
|
||||
|
||||
snap-validate-service() {
|
||||
df_print_func_name "Limine-Snapper-Sync Service Validation"
|
||||
|
||||
echo -e "${DF_BLUE}==>${DF_NC} Checking service unit"
|
||||
|
||||
if systemctl list-unit-files | grep -q "limine-snapper-sync.service"; then
|
||||
echo -e "${DF_GREEN}✓${DF_NC} limine-snapper-sync.service unit exists"
|
||||
else
|
||||
echo -e "${DF_RED}✗${DF_NC} limine-snapper-sync.service unit NOT found"
|
||||
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"
|
||||
snap-check() {
|
||||
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}')
|
||||
[[ -z "$latest" ]] && { df_print_warning "No snapshots"; return 1; }
|
||||
df_print_info "Latest: #$latest"
|
||||
sudo grep -qP "^\\s*///$latest\\s*│" /boot/limine.conf && \
|
||||
df_print_success "Latest in limine.conf" || df_print_error "Latest NOT in limine.conf"
|
||||
local count=$(sudo grep -cP "^\\s*///\\d+\\s*│" /boot/limine.conf || echo "0")
|
||||
df_print_info "Total entries: $count"
|
||||
}
|
||||
|
||||
# Quick snapshot aliases
|
||||
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'
|
||||
alias snap='snap-create' snapls='snap-list' snaprm='snap-delete' snapcheck='snap-check'
|
||||
|
||||
@@ -1,237 +1,96 @@
|
||||
# ============================================================================
|
||||
# SSH Session Manager with Tmux Integration
|
||||
# ============================================================================
|
||||
# Manage SSH connections with automatic tmux session handling
|
||||
# ============================================================================
|
||||
|
||||
# Source shared colors (with fallback)
|
||||
source "${0:A:h}/../lib/colors.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
|
||||
# ============================================================================
|
||||
source "${0:A:h}/../lib/utils.zsh" 2>/dev/null || \
|
||||
source "$HOME/.dotfiles/zsh/lib/utils.zsh" 2>/dev/null
|
||||
|
||||
typeset -g SSH_PROFILES_FILE="${SSH_PROFILES_FILE:-$HOME/.dotfiles/.ssh-profiles}"
|
||||
typeset -g SSH_AUTO_TMUX="${SSH_AUTO_TMUX:-true}"
|
||||
typeset -g SSH_TMUX_SESSION_PREFIX="${SSH_TMUX_SESSION_PREFIX:-ssh}"
|
||||
typeset -g SSH_SYNC_DOTFILES="${SSH_SYNC_DOTFILES:-ask}"
|
||||
typeset -g SSH_TMUX_PREFIX="${SSH_TMUX_PREFIX:-ssh}"
|
||||
|
||||
# ============================================================================
|
||||
# Helper Functions
|
||||
# ============================================================================
|
||||
|
||||
_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_init() {
|
||||
df_ensure_file "$SSH_PROFILES_FILE" "# SSH Profiles: name|user@host|port|key|options|description"
|
||||
}
|
||||
|
||||
_ssh_parse_profile() {
|
||||
local name="$1"
|
||||
local line=$(grep "^${name}|" "$SSH_PROFILES_FILE" 2>/dev/null | head -1)
|
||||
_ssh_parse() {
|
||||
local line=$(grep "^${1}|" "$SSH_PROFILES_FILE" 2>/dev/null | head -1)
|
||||
[[ -z "$line" ]] && return 1
|
||||
IFS='|' read -r profile_name connection port key_file ssh_opts description <<< "$line"
|
||||
echo "$connection|$port|$key_file|$ssh_opts|$description"
|
||||
echo "$line" | cut -d'|' -f2-
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# SSH Profile Management
|
||||
# ============================================================================
|
||||
|
||||
ssh-save() {
|
||||
local name="$1" connection="$2" port="${3:-22}" key_file="${4:-}" options="${5:-}" description="${6:-}"
|
||||
|
||||
_ssh_init_profiles
|
||||
|
||||
[[ -z "$name" || -z "$connection" ]] && {
|
||||
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
|
||||
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
|
||||
grep -q "^${name}|" "$SSH_PROFILES_FILE" 2>/dev/null && {
|
||||
df_confirm "Overwrite '$name'?" || return 1
|
||||
grep -v "^${name}|" "$SSH_PROFILES_FILE" > "${SSH_PROFILES_FILE}.tmp"
|
||||
mv "${SSH_PROFILES_FILE}.tmp" "$SSH_PROFILES_FILE"
|
||||
fi
|
||||
|
||||
echo "${name}|${connection}|${port}|${key_file}|${options}|${description}" >> "$SSH_PROFILES_FILE"
|
||||
|
||||
_ssh_print_success "Saved SSH profile: $name"
|
||||
echo " Connection: $connection"
|
||||
[[ "$port" != "22" ]] && echo " Port: $port"
|
||||
[[ -n "$key_file" ]] && echo " Key: $key_file"
|
||||
}
|
||||
echo "${name}|${conn}|${port}|${key}|${opts}|${desc}" >> "$SSH_PROFILES_FILE"
|
||||
df_print_success "Saved: $name → $conn"
|
||||
}
|
||||
|
||||
ssh-list() {
|
||||
_ssh_init_profiles
|
||||
|
||||
df_print_func_name "SSH Connection Profiles"
|
||||
|
||||
local has_profiles=false
|
||||
while IFS='|' read -r name connection port key options description; do
|
||||
[[ "$name" =~ ^# ]] && continue
|
||||
[[ -z "$name" ]] && continue
|
||||
has_profiles=true
|
||||
|
||||
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
|
||||
_ssh_init
|
||||
df_print_func_name "SSH Profiles"
|
||||
local found=false
|
||||
while IFS='|' read -r name conn port key opts desc; do
|
||||
[[ "$name" =~ ^# || -z "$name" ]] && continue
|
||||
found=true
|
||||
df_print_indent "● $name → $conn"
|
||||
[[ "$port" != "22" && -n "$port" ]] && df_print_indent " Port: $port"
|
||||
[[ -n "$desc" ]] && df_print_indent " $desc"
|
||||
done < "$SSH_PROFILES_FILE"
|
||||
|
||||
[[ "$has_profiles" != true ]] && {
|
||||
_ssh_print_info "No profiles saved yet"
|
||||
echo "Create a profile with: ssh-save myserver user@example.com"
|
||||
}
|
||||
[[ "$found" != true ]] && df_print_info "No profiles. Use: ssh-save name user@host"
|
||||
}
|
||||
|
||||
ssh-delete() {
|
||||
local name="$1"
|
||||
[[ -z "$name" ]] && { echo "Usage: ssh-delete <name>"; return 1; }
|
||||
|
||||
_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"
|
||||
[[ -z "$1" ]] && { echo "Usage: ssh-delete <n>"; 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"
|
||||
mv "${SSH_PROFILES_FILE}.tmp" "$SSH_PROFILES_FILE"
|
||||
_ssh_print_success "Deleted profile: $name"
|
||||
df_print_success "Deleted: $1"
|
||||
}
|
||||
|
||||
ssh-connect() {
|
||||
local name="$1"
|
||||
local session_name="${2:-${SSH_TMUX_SESSION_PREFIX}-${name}}"
|
||||
local name="$1" session="${2:-${SSH_TMUX_PREFIX}-${1}}"
|
||||
[[ -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 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"
|
||||
|
||||
_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"
|
||||
local cmd="ssh"
|
||||
[[ -n "$port" && "$port" != "22" ]] && cmd="$cmd -p $port"
|
||||
[[ -n "$key" ]] && cmd="$cmd -i $key"
|
||||
[[ -n "$opts" ]] && cmd="$cmd $opts"
|
||||
cmd="$cmd $conn"
|
||||
|
||||
if [[ "$SSH_AUTO_TMUX" == "true" ]]; then
|
||||
_ssh_print_info "Attaching to tmux session: $session_name"
|
||||
local tmux_cmd="tmux attach-session -t $session_name 2>/dev/null || tmux new-session -s $session_name"
|
||||
eval "$ssh_cmd -t '$tmux_cmd'"
|
||||
df_print_info "Tmux session: $session"
|
||||
eval "$cmd -t 'tmux attach -t $session 2>/dev/null || tmux new -s $session'"
|
||||
else
|
||||
eval "$ssh_cmd"
|
||||
eval "$cmd"
|
||||
fi
|
||||
}
|
||||
|
||||
sshf() {
|
||||
if ! command -v fzf &>/dev/null; then
|
||||
_ssh_print_error "fzf not installed"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_ssh_init_profiles
|
||||
|
||||
df_require_cmd fzf || return 1
|
||||
_ssh_init
|
||||
local profiles=()
|
||||
while IFS='|' read -r name connection port key options description; do
|
||||
[[ "$name" =~ ^# ]] && continue
|
||||
[[ -z "$name" ]] && continue
|
||||
local display="$name → $connection"
|
||||
[[ -n "$description" ]] && display="$display ($description)"
|
||||
profiles+=("$name|$display")
|
||||
while IFS='|' read -r name conn port key opts desc; do
|
||||
[[ "$name" =~ ^# || -z "$name" ]] && continue
|
||||
profiles+=("$name|$name → $conn")
|
||||
done < "$SSH_PROFILES_FILE"
|
||||
|
||||
[[ ${#profiles[@]} -eq 0 ]] && { _ssh_print_info "No profiles saved"; return 1; }
|
||||
|
||||
local selection=$(printf '%s\n' "${profiles[@]}" | \
|
||||
fzf --height=50% --layout=reverse --border=rounded --prompt='SSH > ' \
|
||||
--delimiter='|' --with-nth=2)
|
||||
|
||||
[[ -n "$selection" ]] && ssh-connect "${selection%%|*}"
|
||||
[[ ${#profiles[@]} -eq 0 ]] && { df_print_info "No profiles"; return 1; }
|
||||
local sel=$(printf '%s\n' "${profiles[@]}" | fzf $(df_fzf_opts) --delimiter='|' --with-nth=2 --prompt='SSH > ')
|
||||
[[ -n "$sel" ]] && ssh-connect "${sel%%|*}"
|
||||
}
|
||||
|
||||
ssh-reconnect() {
|
||||
local name="${1:-last}"
|
||||
|
||||
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
|
||||
alias sshl='ssh-list' sshs='ssh-save' sshc='ssh-connect' sshd='ssh-delete'
|
||||
_ssh_init
|
||||
|
||||
@@ -1,335 +1,124 @@
|
||||
# ============================================================================
|
||||
# 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/colors.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'
|
||||
}
|
||||
source "${0:A:h}/../lib/utils.zsh" 2>/dev/null || \
|
||||
source "$HOME/.dotfiles/zsh/lib/utils.zsh" 2>/dev/null
|
||||
|
||||
# ============================================================================
|
||||
# Core Systemctl Shortcuts
|
||||
# ============================================================================
|
||||
# Core 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() {
|
||||
local service="$1"
|
||||
[[ -z "$service" ]] && { echo "Usage: scr <service>"; return 1; }
|
||||
|
||||
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
|
||||
[[ -z "$1" ]] && { 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"
|
||||
}
|
||||
|
||||
# Enable and start service
|
||||
sce() {
|
||||
local service="$1"
|
||||
[[ -z "$service" ]] && { echo "Usage: sce <service>"; return 1; }
|
||||
|
||||
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
|
||||
[[ -z "$1" ]] && { 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"
|
||||
}
|
||||
|
||||
# Disable and stop service
|
||||
scd() {
|
||||
local service="$1"
|
||||
[[ -z "$service" ]] && { echo "Usage: scd <service>"; return 1; }
|
||||
|
||||
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
|
||||
[[ -z "$1" ]] && { echo "Usage: scd <service>"; return 1; }
|
||||
df_print_step "Disabling $1..."
|
||||
sudo systemctl disable --now "$1" && df_print_success "Disabled" || df_print_error "Failed"
|
||||
}
|
||||
|
||||
# Follow journal logs for a service
|
||||
sclog() {
|
||||
local service="$1"
|
||||
local lines="${2:-50}"
|
||||
[[ -z "$service" ]] && { echo "Usage: sclog <service> [lines]"; return 1; }
|
||||
|
||||
echo -e "${DF_BLUE}==>${DF_NC} Following logs for ${service} (Ctrl+C to exit)..."
|
||||
sudo journalctl -xeu "$service" -f -n "$lines"
|
||||
[[ -z "$1" ]] && { echo "Usage: sclog <service>"; return 1; }
|
||||
df_print_step "Following logs for $1 (Ctrl+C to exit)..."
|
||||
sudo journalctl -xeu "$1" -f -n "${2:-50}"
|
||||
}
|
||||
|
||||
# Show recent logs for a service (without follow)
|
||||
sclogs() {
|
||||
local service="$1"
|
||||
local lines="${2:-50}"
|
||||
[[ -z "$service" ]] && { echo "Usage: sclogs <service> [lines]"; return 1; }
|
||||
|
||||
sudo journalctl -xeu "$service" -n "$lines" --no-pager
|
||||
[[ -z "$1" ]] && { echo "Usage: sclogs <service>"; return 1; }
|
||||
sudo journalctl -xeu "$1" -n "${2:-50}" --no-pager
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Service Status Commands
|
||||
# ============================================================================
|
||||
|
||||
# Show failed services (system and user)
|
||||
sc-failed() {
|
||||
df_print_func_name "Failed Services"
|
||||
|
||||
echo -e "${DF_CYAN}System Services:${DF_NC}"
|
||||
local sys_failed=$(systemctl --failed --no-pager --no-legend 2>/dev/null)
|
||||
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
|
||||
|
||||
df_print_section "System"
|
||||
local sys=$(systemctl --failed --no-pager --no-legend 2>/dev/null)
|
||||
[[ -z "$sys" ]] && df_print_indent "✓ None" || echo "$sys" | sed 's/^/ /'
|
||||
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() {
|
||||
df_print_func_name "Active Timers"
|
||||
|
||||
echo -e "${DF_CYAN}System Timers:${DF_NC}"
|
||||
systemctl list-timers --no-pager | head -20
|
||||
|
||||
echo -e "\n${DF_CYAN}User Timers:${DF_NC}"
|
||||
df_print_section "System"
|
||||
systemctl list-timers --no-pager | head -15
|
||||
echo ""
|
||||
df_print_section "User"
|
||||
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() {
|
||||
df_print_func_name "Boot Time Analysis"
|
||||
|
||||
echo -e "${DF_CYAN}Boot Summary:${DF_NC}"
|
||||
df_print_func_name "Boot Analysis"
|
||||
df_print_section "Summary"
|
||||
systemd-analyze
|
||||
|
||||
echo -e "\n${DF_CYAN}Slowest Services (top 10):${DF_NC}"
|
||||
echo ""
|
||||
df_print_section "Slowest (top 10)"
|
||||
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() {
|
||||
local query="$1"
|
||||
[[ -z "$query" ]] && { echo "Usage: sc-search <query>"; return 1; }
|
||||
|
||||
df_print_func_name "Service Search: $query"
|
||||
|
||||
systemctl list-unit-files --type=service --no-pager | grep -i "$query"
|
||||
[[ -z "$1" ]] && { 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"
|
||||
}
|
||||
|
||||
# Show detailed service info
|
||||
sc-info() {
|
||||
local service="$1"
|
||||
[[ -z "$service" ]] && { echo "Usage: sc-info <service>"; return 1; }
|
||||
|
||||
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
|
||||
|
||||
[[ -z "$1" ]] && { 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
|
||||
echo ""
|
||||
df_print_section "Unit File"
|
||||
systemctl cat "$1" 2>/dev/null | head -30
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Quick Status for MOTD Integration
|
||||
# ============================================================================
|
||||
|
||||
# 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
|
||||
# fzf interactive
|
||||
if df_cmd_exists fzf; then
|
||||
scf() {
|
||||
local service=$(systemctl list-units --type=service --no-pager --no-legend | \
|
||||
awk '{print $1, $2, $3, $4}' | \
|
||||
fzf --height=50% --layout=reverse --border=rounded \
|
||||
--prompt='Service > ' \
|
||||
--preview='systemctl status {1} --no-pager' \
|
||||
--preview-window=right:50%:wrap | \
|
||||
awk '{print $1}')
|
||||
|
||||
if [[ -n "$service" ]]; then
|
||||
echo -e "${DF_BLUE}Selected:${DF_NC} $service"
|
||||
echo ""
|
||||
echo "Actions: [s]tatus [r]estart [o]stop [l]ogs [e]nable [d]isable [q]uit"
|
||||
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
|
||||
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"
|
||||
local svc=$(systemctl list-units --type=service --no-pager --no-legend | \
|
||||
fzf $(df_fzf_opts) --prompt='Service > ' --preview='systemctl status {1} --no-pager' | awk '{print $1}')
|
||||
[[ -z "$svc" ]] && return
|
||||
df_print_info "Selected: $svc"
|
||||
echo "[s]tatus [r]estart [l]ogs [e]nable [d]isable"
|
||||
read -k 1 "act?Action: "; echo
|
||||
case "$act" in
|
||||
s) sudo systemctl status "$svc" --no-pager -l ;;
|
||||
r) scr "$svc" ;;
|
||||
l) sclog "$svc" ;;
|
||||
e) sce "$svc" ;;
|
||||
d) scd "$svc" ;;
|
||||
esac
|
||||
}
|
||||
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() {
|
||||
df_print_func_name "Systemd Helper Commands"
|
||||
|
||||
df_print_func_name "Systemd Commands"
|
||||
cat << 'EOF'
|
||||
|
||||
Core Commands:
|
||||
sc <args> sudo systemctl <args>
|
||||
scu <args> systemctl --user <args>
|
||||
scr <service> Restart and show status
|
||||
sce <service> Enable and start (--now)
|
||||
scd <service> Disable and stop (--now)
|
||||
sclog <service> Follow journal logs (-f)
|
||||
sclogs <service> Show recent logs (no follow)
|
||||
|
||||
Status Commands:
|
||||
sc-failed Show failed services
|
||||
sc-timers Show active timers
|
||||
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
|
||||
|
||||
sc <args> sudo systemctl
|
||||
scu <args> systemctl --user
|
||||
scr <svc> Restart + status
|
||||
sce <svc> Enable + start
|
||||
scd <svc> Disable + stop
|
||||
sclog <svc> Follow logs
|
||||
sclogs <svc> Recent logs
|
||||
sc-failed Failed services
|
||||
sc-timers Active timers
|
||||
sc-boot Boot analysis
|
||||
sc-search <q> Search services
|
||||
sc-info <svc> Service details
|
||||
scf Interactive (fzf)
|
||||
EOF
|
||||
}
|
||||
|
||||
alias scs='sc status' scstart='sc start' scstop='sc stop' screload='sc daemon-reload'
|
||||
alias jctl='journalctl' jctlf='journalctl -f' jctlb='journalctl -b'
|
||||
|
||||
@@ -1,380 +1,116 @@
|
||||
# ============================================================================
|
||||
# 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/colors.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'
|
||||
source "${0:A:h}/../lib/utils.zsh" 2>/dev/null || \
|
||||
source "$HOME/.dotfiles/zsh/lib/utils.zsh" 2>/dev/null
|
||||
|
||||
typeset -g TW_TEMPLATES="${TW_TEMPLATES:-$HOME/.dotfiles/.tmux-templates}"
|
||||
typeset -g TW_PREFIX="${TW_PREFIX:-work}"
|
||||
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_init_templates
|
||||
|
||||
df_print_func_name "Available Tmux Templates"
|
||||
|
||||
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"
|
||||
_tw_init
|
||||
df_print_func_name "Tmux Templates"
|
||||
for t in "$TW_TEMPLATES"/*.tmux; do
|
||||
[[ -f "$t" ]] && df_print_indent "● $(basename "$t" .tmux)"
|
||||
done
|
||||
|
||||
echo
|
||||
echo "Create workspace: ${DF_CYAN}tw-create myproject dev${DF_NC}"
|
||||
echo "Quick attach: ${DF_CYAN}tw myproject${DF_NC}"
|
||||
echo ""
|
||||
df_print_info "Create: tw-create <name> <template>"
|
||||
}
|
||||
|
||||
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() {
|
||||
local workspace_name="$1"
|
||||
local template="${2:-$TW_DEFAULT_TEMPLATE}"
|
||||
local name="$1" tmpl="${2:-$TW_DEFAULT}"
|
||||
[[ -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
|
||||
_tw_init_templates
|
||||
local tfile="$TW_TEMPLATES/${tmpl}.tmux"
|
||||
[[ ! -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
|
||||
_tw_print_error "Workspace '$workspace_name' already exists"
|
||||
echo "Use: ${DF_CYAN}tw $workspace_name${DF_NC} to attach"
|
||||
return 1
|
||||
fi
|
||||
df_in_git_repo && {
|
||||
local root=$(df_git_root)
|
||||
df_print_info "Git root: $root"
|
||||
tmux send-keys -t "$session:0" "cd $root" C-m
|
||||
}
|
||||
|
||||
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() {
|
||||
local workspace_name="$1"
|
||||
[[ -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
|
||||
df_print_success "Created: $name"
|
||||
[[ -z "$TMUX" ]] && tmux attach -t "$session" || df_print_info "Switch: tmux switch -t $session"
|
||||
}
|
||||
|
||||
tw-list() {
|
||||
_tw_check_tmux || return 1
|
||||
|
||||
df_print_func_name "Active Tmux Workspaces"
|
||||
|
||||
local has_workspaces=false
|
||||
|
||||
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
|
||||
_tw_check || return 1
|
||||
df_print_func_name "Tmux Workspaces"
|
||||
local found=false
|
||||
tmux list-sessions 2>/dev/null | while IFS=: read -r sess rest; do
|
||||
[[ "$sess" == ${TW_PREFIX}-* ]] && { found=true; df_print_indent "● ${sess#${TW_PREFIX}-}"; }
|
||||
done
|
||||
|
||||
if [[ "$has_workspaces" != true ]]; then
|
||||
_tw_print_info "No active workspaces"
|
||||
echo "Create one with: ${DF_CYAN}tw-create myproject${DF_NC}"
|
||||
fi
|
||||
[[ "$found" != true ]] && df_print_info "No workspaces. Use: tw-create <name>"
|
||||
}
|
||||
|
||||
tw-attach() {
|
||||
local name="$1"
|
||||
[[ -z "$name" ]] && { tw-list; return 1; }
|
||||
_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() {
|
||||
local workspace_name="$1"
|
||||
[[ -z "$workspace_name" ]] && { echo "Usage: tw-delete <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"
|
||||
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}"
|
||||
[[ -z "$1" ]] && { tw-list; return 1; }
|
||||
_tw_check || return 1
|
||||
local session="${TW_PREFIX}-${1}"
|
||||
tmux has-session -t "$session" 2>/dev/null || { df_print_error "'$1' not found"; return 1; }
|
||||
tmux kill-session -t "$session"
|
||||
df_print_success "Deleted: $1"
|
||||
}
|
||||
|
||||
tw() {
|
||||
local workspace_name="$1"
|
||||
local template="${2:-$TW_DEFAULT_TEMPLATE}"
|
||||
|
||||
[[ -z "$workspace_name" ]] && { tw-list; return 0; }
|
||||
|
||||
_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
|
||||
local name="$1" tmpl="${2:-$TW_DEFAULT}"
|
||||
[[ -z "$name" ]] && { tw-list; return; }
|
||||
_tw_check || return 1
|
||||
local session="${TW_PREFIX}-${name}"
|
||||
tmux has-session -t "$session" 2>/dev/null && tw-attach "$name" || tw-create "$name" "$tmpl"
|
||||
}
|
||||
|
||||
twf() {
|
||||
if ! command -v fzf &>/dev/null; then
|
||||
_tw_print_error "fzf not installed"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_tw_check_tmux || return 1
|
||||
|
||||
df_require_cmd fzf || return 1
|
||||
_tw_check || return 1
|
||||
local sessions=()
|
||||
tmux list-sessions 2>/dev/null | while IFS=: read -r session_full rest; do
|
||||
if [[ "$session_full" == ${TW_SESSION_PREFIX}-* ]]; then
|
||||
local workspace_name="${session_full#${TW_SESSION_PREFIX}-}"
|
||||
sessions+=("$workspace_name")
|
||||
fi
|
||||
tmux list-sessions 2>/dev/null | while IFS=: read -r sess rest; do
|
||||
[[ "$sess" == ${TW_PREFIX}-* ]] && sessions+=("${sess#${TW_PREFIX}-}")
|
||||
done
|
||||
|
||||
[[ ${#sessions[@]} -eq 0 ]] && { _tw_print_info "No workspaces found"; return 1; }
|
||||
|
||||
local selection=$(printf '%s\n' "${sessions[@]}" | \
|
||||
fzf --height=40% --layout=reverse --border=rounded --prompt='Workspace > ')
|
||||
|
||||
[[ -n "$selection" ]] && tw-attach "$selection"
|
||||
[[ ${#sessions[@]} -eq 0 ]] && { df_print_info "No workspaces"; return 1; }
|
||||
local sel=$(printf '%s\n' "${sessions[@]}" | fzf $(df_fzf_opts) --prompt='Workspace > ')
|
||||
[[ -n "$sel" ]] && tw-attach "$sel"
|
||||
}
|
||||
|
||||
tw-sync() {
|
||||
[[ -z "$TMUX" ]] && { _tw_print_error "Must be run from inside tmux"; return 1; }
|
||||
|
||||
local current=$(tmux show-window-option -v synchronize-panes 2>/dev/null)
|
||||
|
||||
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
|
||||
[[ -z "$TMUX" ]] && { df_print_error "Must be in tmux"; return 1; }
|
||||
local cur=$(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"; }
|
||||
}
|
||||
|
||||
tw-rename() {
|
||||
local old_name="$1"
|
||||
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
|
||||
alias twl='tw-list' twc='tw-create' twa='tw-attach' twd='tw-delete' twt='tw-templates'
|
||||
_tw_init
|
||||
|
||||
Reference in New Issue
Block a user