Dotfiles update 2025-12-25 10:23
This commit is contained in:
@@ -2,429 +2,209 @@
|
|||||||
# Btrfs Helpers for Arch/CachyOS
|
# Btrfs Helpers for Arch/CachyOS
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Quick commands for btrfs filesystem management
|
# Quick commands for btrfs filesystem management
|
||||||
# CachyOS defaults to btrfs, so these are highly useful
|
|
||||||
#
|
|
||||||
# Commands:
|
|
||||||
# btrfs-usage - Show filesystem usage
|
|
||||||
# btrfs-subs - List subvolumes
|
|
||||||
# btrfs-balance - Start balance operation
|
|
||||||
# btrfs-scrub - Start/check scrub
|
|
||||||
# btrfs-defrag - Defragment file or directory
|
|
||||||
# btrfs-compress - Show compression stats
|
|
||||||
# btrfs-info - Full filesystem info
|
|
||||||
# btrfs-health - Quick health check
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
# Source shared colors (with fallback)
|
source "${0:A:h}/../lib/utils.zsh" 2>/dev/null || \
|
||||||
source "${0:A:h}/../lib/colors.zsh" 2>/dev/null || \
|
source "$HOME/.dotfiles/zsh/lib/utils.zsh" 2>/dev/null
|
||||||
source "$HOME/.dotfiles/zsh/lib/colors.zsh" 2>/dev/null || {
|
|
||||||
typeset -g DF_GREEN=$'\033[0;32m' DF_YELLOW=$'\033[1;33m'
|
|
||||||
typeset -g DF_RED=$'\033[0;31m' DF_BLUE=$'\033[0;34m'
|
|
||||||
typeset -g DF_CYAN=$'\033[0;36m' DF_NC=$'\033[0m'
|
|
||||||
}
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Configuration
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
typeset -g BTRFS_DEFAULT_MOUNT="${BTRFS_DEFAULT_MOUNT:-/}"
|
typeset -g BTRFS_DEFAULT_MOUNT="${BTRFS_DEFAULT_MOUNT:-/}"
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Detection
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
_btrfs_check() {
|
_btrfs_check() {
|
||||||
if ! command -v btrfs &>/dev/null; then
|
df_require_cmd btrfs btrfs-progs || return 1
|
||||||
echo -e "${DF_RED}✗${DF_NC} btrfs-progs not installed"
|
if ! df_is_btrfs; then
|
||||||
echo "Install: sudo pacman -S btrfs-progs"
|
df_print_warning "Root filesystem is not btrfs"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check if root is btrfs
|
|
||||||
local fstype=$(df -T / | awk 'NR==2 {print $2}')
|
|
||||||
if [[ "$fstype" != "btrfs" ]]; then
|
|
||||||
echo -e "${DF_YELLOW}⚠${DF_NC} Root filesystem is not btrfs (detected: $fstype)"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Core Commands
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
# Show filesystem usage
|
|
||||||
btrfs-usage() {
|
btrfs-usage() {
|
||||||
_btrfs_check || return 1
|
_btrfs_check || return 1
|
||||||
local mount="${1:-$BTRFS_DEFAULT_MOUNT}"
|
local mount="${1:-$BTRFS_DEFAULT_MOUNT}"
|
||||||
|
|
||||||
df_print_func_name "Btrfs Filesystem Usage: ${mount}"
|
df_print_func_name "Btrfs Filesystem Usage: ${mount}"
|
||||||
|
|
||||||
sudo btrfs filesystem usage "$mount" -h
|
sudo btrfs filesystem usage "$mount" -h
|
||||||
}
|
}
|
||||||
|
|
||||||
# List all subvolumes
|
|
||||||
btrfs-subs() {
|
btrfs-subs() {
|
||||||
_btrfs_check || return 1
|
_btrfs_check || return 1
|
||||||
local mount="${1:-$BTRFS_DEFAULT_MOUNT}"
|
local mount="${1:-$BTRFS_DEFAULT_MOUNT}"
|
||||||
|
|
||||||
df_print_func_name "Btrfs Subvolumes"
|
df_print_func_name "Btrfs Subvolumes"
|
||||||
|
df_print_section "Subvolume List"
|
||||||
echo -e "${DF_CYAN}Subvolume List:${DF_NC}"
|
|
||||||
sudo btrfs subvolume list "$mount" | while read -r line; do
|
sudo btrfs subvolume list "$mount" | while read -r line; do
|
||||||
local path=$(echo "$line" | awk '{print $NF}')
|
local path=$(echo "$line" | awk '{print $NF}')
|
||||||
local id=$(echo "$line" | awk '{print $2}')
|
local id=$(echo "$line" | awk '{print $2}')
|
||||||
echo -e " ${DF_GREEN}●${DF_NC} [$id] $path"
|
df_print_indent "● [$id] $path"
|
||||||
done
|
done
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${DF_CYAN}Default Subvolume:${DF_NC}"
|
df_print_section "Default Subvolume"
|
||||||
sudo btrfs subvolume get-default "$mount"
|
sudo btrfs subvolume get-default "$mount"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Start balance operation
|
|
||||||
btrfs-balance() {
|
btrfs-balance() {
|
||||||
_btrfs_check || return 1
|
_btrfs_check || return 1
|
||||||
local mount="${1:-$BTRFS_DEFAULT_MOUNT}"
|
local mount="${1:-$BTRFS_DEFAULT_MOUNT}"
|
||||||
local usage="${2:-50}" # Default: rebalance chunks with <50% usage
|
local usage="${2:-50}"
|
||||||
|
|
||||||
df_print_func_name "Btrfs Balance"
|
df_print_func_name "Btrfs Balance"
|
||||||
|
df_confirm_warning "This may take a while and use significant I/O" || return 0
|
||||||
echo -e "${DF_YELLOW}⚠${DF_NC} This may take a while and use significant I/O"
|
|
||||||
echo ""
|
echo ""
|
||||||
|
df_print_step "Balancing data chunks with <${usage}% usage..."
|
||||||
read -q "REPLY?Continue? [y/N]: "; echo
|
|
||||||
[[ ! "$REPLY" =~ ^[Yy]$ ]] && return 0
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo -e "${DF_BLUE}==>${DF_NC} Balancing data chunks with <${usage}% usage..."
|
|
||||||
sudo btrfs balance start -dusage="$usage" -musage="$usage" "$mount" -v
|
sudo btrfs balance start -dusage="$usage" -musage="$usage" "$mount" -v
|
||||||
|
[[ $? -eq 0 ]] && df_print_success "Balance completed" || df_print_warning "Balance finished (may have been interrupted)"
|
||||||
if [[ $? -eq 0 ]]; then
|
|
||||||
echo -e "${DF_GREEN}✓${DF_NC} Balance completed"
|
|
||||||
else
|
|
||||||
echo -e "${DF_YELLOW}⚠${DF_NC} Balance finished (may have been interrupted or had no work)"
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Check balance status
|
|
||||||
btrfs-balance-status() {
|
btrfs-balance-status() {
|
||||||
_btrfs_check || return 1
|
_btrfs_check || return 1
|
||||||
local mount="${1:-$BTRFS_DEFAULT_MOUNT}"
|
|
||||||
|
|
||||||
df_print_func_name "Btrfs Balance Status"
|
df_print_func_name "Btrfs Balance Status"
|
||||||
|
sudo btrfs balance status "${1:-$BTRFS_DEFAULT_MOUNT}"
|
||||||
sudo btrfs balance status "$mount"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Cancel running balance
|
|
||||||
btrfs-balance-cancel() {
|
btrfs-balance-cancel() {
|
||||||
_btrfs_check || return 1
|
_btrfs_check || return 1
|
||||||
local mount="${1:-$BTRFS_DEFAULT_MOUNT}"
|
df_print_step "Cancelling balance..."
|
||||||
|
sudo btrfs balance cancel "${1:-$BTRFS_DEFAULT_MOUNT}"
|
||||||
echo -e "${DF_BLUE}==>${DF_NC} Cancelling balance on ${mount}..."
|
|
||||||
sudo btrfs balance cancel "$mount"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Start scrub operation
|
|
||||||
btrfs-scrub() {
|
btrfs-scrub() {
|
||||||
_btrfs_check || return 1
|
_btrfs_check || return 1
|
||||||
local mount="${1:-$BTRFS_DEFAULT_MOUNT}"
|
local mount="${1:-$BTRFS_DEFAULT_MOUNT}"
|
||||||
|
|
||||||
df_print_func_name "Btrfs Scrub"
|
df_print_func_name "Btrfs Scrub"
|
||||||
|
|
||||||
# Check if scrub is already running
|
|
||||||
local status=$(sudo btrfs scrub status "$mount" 2>/dev/null)
|
local status=$(sudo btrfs scrub status "$mount" 2>/dev/null)
|
||||||
if echo "$status" | grep -q "running"; then
|
if echo "$status" | grep -q "running"; then
|
||||||
echo -e "${DF_CYAN}Scrub Status (running):${DF_NC}"
|
df_print_section "Scrub Status (running)"
|
||||||
echo "$status" | sed 's/^/ /'
|
echo "$status" | sed 's/^/ /'
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
df_print_warning "Scrub verifies data integrity and may take hours"
|
||||||
echo -e "${DF_YELLOW}⚠${DF_NC} Scrub verifies data integrity and may take hours"
|
df_confirm "Start scrub?" || return 0
|
||||||
read -q "REPLY?Start scrub? [y/N]: "; echo
|
df_print_step "Starting scrub..."
|
||||||
[[ ! "$REPLY" =~ ^[Yy]$ ]] && return 0
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo -e "${DF_BLUE}==>${DF_NC} Starting scrub..."
|
|
||||||
sudo btrfs scrub start "$mount"
|
sudo btrfs scrub start "$mount"
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${DF_CYAN}Scrub Status:${DF_NC}"
|
df_print_section "Scrub Status"
|
||||||
sudo btrfs scrub status "$mount"
|
sudo btrfs scrub status "$mount"
|
||||||
|
df_print_info "Monitor with: btrfs-scrub-status"
|
||||||
echo ""
|
|
||||||
echo -e "${DF_CYAN}Monitor with:${DF_NC} btrfs-scrub-status"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Show scrub status
|
|
||||||
btrfs-scrub-status() {
|
btrfs-scrub-status() {
|
||||||
_btrfs_check || return 1
|
_btrfs_check || return 1
|
||||||
local mount="${1:-$BTRFS_DEFAULT_MOUNT}"
|
|
||||||
|
|
||||||
df_print_func_name "Btrfs Scrub Status"
|
df_print_func_name "Btrfs Scrub Status"
|
||||||
|
sudo btrfs scrub status "${1:-$BTRFS_DEFAULT_MOUNT}"
|
||||||
sudo btrfs scrub status "$mount"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Cancel scrub
|
|
||||||
btrfs-scrub-cancel() {
|
btrfs-scrub-cancel() {
|
||||||
_btrfs_check || return 1
|
_btrfs_check || return 1
|
||||||
local mount="${1:-$BTRFS_DEFAULT_MOUNT}"
|
df_print_step "Cancelling scrub..."
|
||||||
|
sudo btrfs scrub cancel "${1:-$BTRFS_DEFAULT_MOUNT}"
|
||||||
echo -e "${DF_BLUE}==>${DF_NC} Cancelling scrub on ${mount}..."
|
|
||||||
sudo btrfs scrub cancel "$mount"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Defragment file or directory
|
|
||||||
btrfs-defrag() {
|
btrfs-defrag() {
|
||||||
_btrfs_check || return 1
|
_btrfs_check || return 1
|
||||||
local target="${1:-.}"
|
local target="${1:-.}"
|
||||||
|
[[ ! -e "$target" ]] && { df_print_error "Target not found: $target"; return 1; }
|
||||||
if [[ ! -e "$target" ]]; then
|
|
||||||
echo -e "${DF_RED}✗${DF_NC} Target not found: $target"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
df_print_func_name "Btrfs Defragment"
|
df_print_func_name "Btrfs Defragment"
|
||||||
|
|
||||||
if [[ -d "$target" ]]; then
|
if [[ -d "$target" ]]; then
|
||||||
echo -e "${DF_YELLOW}⚠${DF_NC} Recursive defrag on directory: $target"
|
df_print_warning "Recursive defrag on directory: $target"
|
||||||
read -q "REPLY?Continue? [y/N]: "; echo
|
df_confirm "Continue?" || return 0
|
||||||
[[ ! "$REPLY" =~ ^[Yy]$ ]] && return 0
|
|
||||||
|
|
||||||
sudo btrfs filesystem defragment -r -v "$target"
|
sudo btrfs filesystem defragment -r -v "$target"
|
||||||
else
|
else
|
||||||
echo -e "${DF_BLUE}==>${DF_NC} Defragmenting: $target"
|
df_print_step "Defragmenting: $target"
|
||||||
sudo btrfs filesystem defragment -v "$target"
|
sudo btrfs filesystem defragment -v "$target"
|
||||||
fi
|
fi
|
||||||
|
df_print_success "Defragmentation complete"
|
||||||
echo -e "${DF_GREEN}✓${DF_NC} Defragmentation complete"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Show compression stats (requires compsize)
|
|
||||||
btrfs-compress() {
|
btrfs-compress() {
|
||||||
_btrfs_check || return 1
|
_btrfs_check || return 1
|
||||||
local target="${1:-$BTRFS_DEFAULT_MOUNT}"
|
df_require_cmd compsize || return 1
|
||||||
|
|
||||||
if ! command -v compsize &>/dev/null; then
|
|
||||||
echo -e "${DF_YELLOW}⚠${DF_NC} compsize not installed"
|
|
||||||
echo "Install: sudo pacman -S compsize"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
df_print_func_name "Btrfs Compression Statistics"
|
df_print_func_name "Btrfs Compression Statistics"
|
||||||
|
sudo compsize "${1:-$BTRFS_DEFAULT_MOUNT}"
|
||||||
sudo compsize "$target"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Information Commands
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
# Full filesystem info
|
|
||||||
btrfs-info() {
|
btrfs-info() {
|
||||||
_btrfs_check || return 1
|
_btrfs_check || return 1
|
||||||
local mount="${1:-$BTRFS_DEFAULT_MOUNT}"
|
local mount="${1:-$BTRFS_DEFAULT_MOUNT}"
|
||||||
|
|
||||||
df_print_func_name "Btrfs Filesystem Information"
|
df_print_func_name "Btrfs Filesystem Information"
|
||||||
|
df_print_section "Filesystem Show"
|
||||||
echo -e "${DF_CYAN}Filesystem Show:${DF_NC}"
|
|
||||||
sudo btrfs filesystem show "$mount"
|
sudo btrfs filesystem show "$mount"
|
||||||
|
|
||||||
echo -e "\n${DF_CYAN}Filesystem df:${DF_NC}"
|
|
||||||
sudo btrfs filesystem df "$mount"
|
|
||||||
|
|
||||||
echo -e "\n${DF_CYAN}Device Stats:${DF_NC}"
|
|
||||||
sudo btrfs device stats "$mount"
|
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
|
df_print_section "Filesystem df"
|
||||||
|
sudo btrfs filesystem df "$mount"
|
||||||
|
echo ""
|
||||||
|
df_print_section "Device Stats"
|
||||||
|
sudo btrfs device stats "$mount"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Quick health check
|
|
||||||
btrfs-health() {
|
btrfs-health() {
|
||||||
_btrfs_check || return 1
|
_btrfs_check || return 1
|
||||||
local mount="${1:-$BTRFS_DEFAULT_MOUNT}"
|
local mount="${1:-$BTRFS_DEFAULT_MOUNT}"
|
||||||
|
|
||||||
df_print_func_name "Btrfs Health Check"
|
df_print_func_name "Btrfs Health Check"
|
||||||
|
|
||||||
local issues=0
|
local issues=0
|
||||||
|
|
||||||
# Check device stats for errors
|
df_print_section "Device Errors"
|
||||||
echo -e "${DF_CYAN}Device Errors:${DF_NC}"
|
local errors=$(sudo btrfs device stats "$mount" 2>/dev/null | grep -v " 0$" | grep -v "^$")
|
||||||
local stats=$(sudo btrfs device stats "$mount" 2>/dev/null)
|
[[ -z "$errors" ]] && df_print_indent "✓ No errors" || { df_print_indent "✗ Errors detected:"; echo "$errors" | sed 's/^/ /'; ((issues++)); }
|
||||||
local errors=$(echo "$stats" | grep -v " 0$" | grep -v "^$")
|
|
||||||
|
|
||||||
if [[ -z "$errors" ]]; then
|
|
||||||
echo -e " ${DF_GREEN}✓${DF_NC} No device errors detected"
|
|
||||||
else
|
|
||||||
echo -e " ${DF_RED}✗${DF_NC} Errors detected:"
|
|
||||||
echo "$errors" | sed 's/^/ /'
|
|
||||||
issues=$((issues + 1))
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check allocation
|
|
||||||
echo -e "\n${DF_CYAN}Space Allocation:${DF_NC}"
|
|
||||||
local usage=$(sudo btrfs filesystem usage "$mount" -b 2>/dev/null)
|
|
||||||
local used_pct=$(echo "$usage" | grep "Used:" | head -1 | awk '{print $2}' | tr -d '%')
|
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
df_print_section "Space Allocation"
|
||||||
|
local used_pct=$(sudo btrfs filesystem usage "$mount" -b 2>/dev/null | grep "Used:" | head -1 | awk '{print $2}' | tr -d '%')
|
||||||
if [[ -n "$used_pct" ]]; then
|
if [[ -n "$used_pct" ]]; then
|
||||||
if (( used_pct >= 90 )); then
|
(( used_pct >= 90 )) && { df_print_indent "✗ ${used_pct}% full - critical!"; ((issues++)); } || \
|
||||||
echo -e " ${DF_RED}✗${DF_NC} Filesystem ${used_pct}% full - critical!"
|
(( used_pct >= 80 )) && df_print_indent "⚠ ${used_pct}% full" || df_print_indent "✓ ${used_pct}% used"
|
||||||
issues=$((issues + 1))
|
|
||||||
elif (( used_pct >= 80 )); then
|
|
||||||
echo -e " ${DF_YELLOW}⚠${DF_NC} Filesystem ${used_pct}% full - consider cleanup"
|
|
||||||
else
|
|
||||||
echo -e " ${DF_GREEN}✓${DF_NC} Filesystem ${used_pct}% used"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check last scrub
|
|
||||||
echo -e "\n${DF_CYAN}Last Scrub:${DF_NC}"
|
|
||||||
local scrub_status=$(sudo btrfs scrub status "$mount" 2>/dev/null)
|
|
||||||
local scrub_date=$(echo "$scrub_status" | grep "Scrub started" | awk '{print $3, $4, $5}')
|
|
||||||
local scrub_errors=$(echo "$scrub_status" | grep "Error summary" | grep -v "no errors")
|
|
||||||
|
|
||||||
if [[ -n "$scrub_date" ]]; then
|
|
||||||
echo -e " Last scrub: $scrub_date"
|
|
||||||
if [[ -n "$scrub_errors" ]]; then
|
|
||||||
echo -e " ${DF_RED}✗${DF_NC} Scrub found errors"
|
|
||||||
echo "$scrub_errors" | sed 's/^/ /'
|
|
||||||
issues=$((issues + 1))
|
|
||||||
else
|
|
||||||
echo -e " ${DF_GREEN}✓${DF_NC} No errors in last scrub"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo -e " ${DF_YELLOW}⚠${DF_NC} No scrub has been run (recommended monthly)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Summary
|
|
||||||
echo ""
|
|
||||||
if (( issues == 0 )); then
|
|
||||||
echo -e "${DF_GREEN}✓${DF_NC} Btrfs filesystem appears healthy"
|
|
||||||
else
|
|
||||||
echo -e "${DF_RED}✗${DF_NC} Found $issues issue(s) - investigate above"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
|
df_print_section "Last Scrub"
|
||||||
|
local scrub=$(sudo btrfs scrub status "$mount" 2>/dev/null)
|
||||||
|
local scrub_date=$(echo "$scrub" | grep "Scrub started" | awk '{print $3, $4, $5}')
|
||||||
|
[[ -n "$scrub_date" ]] && df_print_indent "Last: $scrub_date" || df_print_indent "⚠ No scrub run yet"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
(( issues == 0 )) && df_print_success "Filesystem healthy" || df_print_error "Found $issues issue(s)"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Snapshot Helpers (complement snapper.zsh)
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
# Show snapshot space usage
|
|
||||||
btrfs-snap-usage() {
|
btrfs-snap-usage() {
|
||||||
_btrfs_check || return 1
|
_btrfs_check || return 1
|
||||||
|
|
||||||
df_print_func_name "Snapshot Disk Space Usage"
|
df_print_func_name "Snapshot Disk Space Usage"
|
||||||
|
|
||||||
if [[ -d "/.snapshots" ]]; then
|
if [[ -d "/.snapshots" ]]; then
|
||||||
echo -e "${DF_CYAN}Snapshot Directory:${DF_NC}"
|
df_print_section "Snapshot Directory"
|
||||||
local size
|
local size=$(timeout 10 sudo du -sh /.snapshots 2>/dev/null | cut -f1)
|
||||||
size=$(sudo du -sh /.snapshots 2>/dev/null | cut -f1)
|
df_print_indent "${size:-Unable to calculate}"
|
||||||
if [[ -n "$size" ]]; then
|
echo ""
|
||||||
echo " $size"
|
df_print_section "Individual Snapshots (top 10)"
|
||||||
else
|
timeout 30 sudo du -sh /.snapshots/*/ 2>/dev/null | sort -h | tail -10 | sed 's/^/ /'
|
||||||
echo " Unable to calculate (timeout or error)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo -e "\n${DF_CYAN}Individual Snapshots (top 10 by size):${DF_NC}"
|
|
||||||
sudo du -sh /.snapshots/*/ 2>/dev/null | sort -h | tail -10 | sed 's/^/ /' || \
|
|
||||||
echo " Unable to list snapshots"
|
|
||||||
else
|
else
|
||||||
echo -e "${DF_YELLOW}⚠${DF_NC} No /.snapshots directory found"
|
df_print_warning "No /.snapshots directory found"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Maintenance
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
# Full maintenance routine
|
|
||||||
btrfs-maintain() {
|
btrfs-maintain() {
|
||||||
_btrfs_check || return 1
|
_btrfs_check || return 1
|
||||||
local mount="${1:-$BTRFS_DEFAULT_MOUNT}"
|
local mount="${1:-$BTRFS_DEFAULT_MOUNT}"
|
||||||
|
|
||||||
df_print_func_name "Btrfs Maintenance Routine"
|
df_print_func_name "Btrfs Maintenance Routine"
|
||||||
|
echo "This will: health check, balance, scrub"
|
||||||
echo "This will perform:"
|
df_confirm_warning "This may take several hours" || return 0
|
||||||
echo " 1. Health check"
|
df_print_step "Step 1/3: Health Check"
|
||||||
echo " 2. Balance (low usage chunks)"
|
|
||||||
echo " 3. Scrub (data integrity)"
|
|
||||||
echo ""
|
|
||||||
echo -e "${DF_YELLOW}⚠${DF_NC} This may take several hours"
|
|
||||||
read -q "REPLY?Continue? [y/N]: "; echo
|
|
||||||
[[ ! "$REPLY" =~ ^[Yy]$ ]] && return 0
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo -e "${DF_BLUE}==>${DF_NC} Step 1/3: Health Check"
|
|
||||||
btrfs-health "$mount"
|
btrfs-health "$mount"
|
||||||
|
df_print_step "Step 2/3: Balance"
|
||||||
echo -e "${DF_BLUE}==>${DF_NC} Step 2/3: Balance"
|
|
||||||
sudo btrfs balance start -dusage=50 -musage=50 "$mount"
|
sudo btrfs balance start -dusage=50 -musage=50 "$mount"
|
||||||
|
df_print_step "Step 3/3: Scrub"
|
||||||
echo ""
|
sudo btrfs scrub start -B "$mount"
|
||||||
echo -e "${DF_BLUE}==>${DF_NC} Step 3/3: Scrub"
|
df_print_success "Maintenance complete"
|
||||||
sudo btrfs scrub start -B "$mount" # -B runs in foreground
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo -e "${DF_GREEN}✓${DF_NC} Maintenance complete"
|
|
||||||
btrfs-health "$mount"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Aliases
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
alias btru='btrfs-usage'
|
|
||||||
alias btrs='btrfs-subs'
|
|
||||||
alias btrh='btrfs-health'
|
|
||||||
alias btri='btrfs-info'
|
|
||||||
alias btrc='btrfs-compress'
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Help
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
btrfs-help() {
|
btrfs-help() {
|
||||||
df_print_func_name "Btrfs Helper Commands"
|
df_print_func_name "Btrfs Helper Commands"
|
||||||
|
|
||||||
cat << 'EOF'
|
cat << 'EOF'
|
||||||
|
btrfs-usage [mount] Filesystem usage
|
||||||
Information:
|
btrfs-subs [mount] List subvolumes
|
||||||
btrfs-usage [mount] Filesystem usage summary
|
btrfs-info [mount] Full filesystem info
|
||||||
btrfs-subs [mount] List all subvolumes
|
btrfs-health [mount] Quick health check
|
||||||
btrfs-info [mount] Full filesystem information
|
btrfs-compress [path] Compression stats
|
||||||
btrfs-health [mount] Quick health check
|
btrfs-balance [mount] Start balance
|
||||||
btrfs-compress [path] Compression statistics (requires compsize)
|
btrfs-scrub [mount] Start scrub
|
||||||
|
btrfs-defrag <path> Defragment
|
||||||
Maintenance:
|
btrfs-snap-usage Snapshot space usage
|
||||||
btrfs-balance [mount] Start balance operation
|
btrfs-maintain [mount] Full maintenance
|
||||||
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
|
|
||||||
|
|
||||||
EOF
|
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
|
# Command Palette - Fuzzy Command Launcher for Zsh
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# A Raycast/Alfred-style command palette for the terminal
|
# A Raycast/Alfred-style command palette for the terminal
|
||||||
#
|
|
||||||
# Features:
|
|
||||||
# - Search aliases, functions, recent commands
|
|
||||||
# - Search bookmarked directories
|
|
||||||
# - Search dotfiles scripts
|
|
||||||
# - Quick actions (edit config, reload shell, etc.)
|
|
||||||
#
|
|
||||||
# Keybinding: Ctrl+Space (configurable)
|
# Keybinding: Ctrl+Space (configurable)
|
||||||
#
|
|
||||||
# Requirements: fzf
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
# Source shared colors (with fallback)
|
source "${0:A:h}/../lib/utils.zsh" 2>/dev/null || \
|
||||||
source "${0:A:h}/../lib/colors.zsh" 2>/dev/null || \
|
source "$HOME/.dotfiles/zsh/lib/utils.zsh" 2>/dev/null
|
||||||
source "$HOME/.dotfiles/zsh/lib/colors.zsh" 2>/dev/null || {
|
|
||||||
typeset -g DF_GREEN=$'\033[0;32m' DF_BLUE=$'\033[0;34m'
|
|
||||||
typeset -g DF_CYAN=$'\033[0;36m' DF_NC=$'\033[0m'
|
|
||||||
}
|
|
||||||
|
|
||||||
# ============================================================================
|
typeset -g PALETTE_HOTKEY="${PALETTE_HOTKEY:-^@}"
|
||||||
# Configuration
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
typeset -g PALETTE_HOTKEY="${PALETTE_HOTKEY:-^@}" # Ctrl+Space
|
|
||||||
typeset -g PALETTE_HISTORY_SIZE=50
|
typeset -g PALETTE_HISTORY_SIZE=50
|
||||||
typeset -g PALETTE_BOOKMARKS_FILE="$HOME/.dotfiles/.bookmarks"
|
typeset -g PALETTE_BOOKMARKS_FILE="$HOME/.dotfiles/.bookmarks"
|
||||||
typeset -g DOTFILES_DIR="${DOTFILES_DIR:-$HOME/.dotfiles}"
|
typeset -g DOTFILES_DIR="${DOTFILES_DIR:-$HOME/.dotfiles}"
|
||||||
|
|
||||||
# Icons (works with most terminals)
|
typeset -g ICON_ALIAS="⚡" ICON_FUNC="λ" ICON_HIST="↺" ICON_DIR="📁"
|
||||||
typeset -g ICON_ALIAS="⚡"
|
typeset -g ICON_SCRIPT="⚙" ICON_ACTION="★" ICON_GIT="⎇"
|
||||||
typeset -g ICON_FUNC="λ"
|
|
||||||
typeset -g ICON_HIST="↺"
|
|
||||||
typeset -g ICON_DIR="📁"
|
|
||||||
typeset -g ICON_SCRIPT="⚙"
|
|
||||||
typeset -g ICON_ACTION="★"
|
|
||||||
typeset -g ICON_GIT="⎇"
|
|
||||||
typeset -g ICON_DOCKER="◉"
|
|
||||||
typeset -g ICON_EDIT="✎"
|
|
||||||
typeset -g ICON_RUN="▶"
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Check Dependencies
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
_palette_check_deps() {
|
|
||||||
if ! command -v fzf &>/dev/null; then
|
|
||||||
echo "Command palette requires fzf."
|
|
||||||
echo "Install: git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf && ~/.fzf/install"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Data Sources
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
_palette_get_aliases() {
|
_palette_get_aliases() {
|
||||||
alias | sed 's/^alias //' | while IFS='=' read -r alias_name cmd; do
|
alias | sed 's/^alias //' | while IFS='=' read -r name cmd; do
|
||||||
cmd="${cmd#\'}"
|
cmd="${cmd#\'}"; cmd="${cmd%\'}"; cmd="${cmd#\"}"; cmd="${cmd%\"}"
|
||||||
cmd="${cmd%\'}"
|
printf "%s\t%s\t%s\t%s\n" "$ICON_ALIAS" "alias" "$name" "$cmd"
|
||||||
cmd="${cmd#\"}"
|
|
||||||
cmd="${cmd%\"}"
|
|
||||||
printf "%s\t%s\t%s\t%s\n" "$ICON_ALIAS" "alias" "$alias_name" "$cmd"
|
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
_palette_get_functions() {
|
_palette_get_functions() {
|
||||||
print -l ${(ok)functions} | grep -v '^_' | while read -r func_name; do
|
print -l ${(ok)functions} | grep -v "^_" | while read -r name; do
|
||||||
printf "%s\t%s\t%s\t%s\n" "$ICON_FUNC" "func" "$func_name" "function"
|
printf "%s\t%s\t%s\t%s\n" "$ICON_FUNC" "function" "$name" ""
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
_palette_get_history() {
|
_palette_get_history() {
|
||||||
fc -ln -$PALETTE_HISTORY_SIZE | tac | awk '!seen[$0]++' | head -30 | while read -r cmd; do
|
fc -ln -"$PALETTE_HISTORY_SIZE" 2>/dev/null | awk '!seen[$0]++' | while read -r cmd; do
|
||||||
[[ -n "$cmd" ]] && printf "%s\t%s\t%s\t%s\n" "$ICON_HIST" "history" "${cmd:0:50}" "$cmd"
|
[[ -n "$cmd" ]] && printf "%s\t%s\t%s\t%s\n" "$ICON_HIST" "history" "$cmd" ""
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
_palette_get_bookmarks() {
|
_palette_get_bookmarks() {
|
||||||
[[ ! -f "$PALETTE_BOOKMARKS_FILE" ]] && return
|
[[ ! -f "$PALETTE_BOOKMARKS_FILE" ]] && return
|
||||||
|
while IFS='|' read -r name path desc; do
|
||||||
while IFS='|' read -r bm_name bm_path; do
|
[[ "$name" =~ ^# || -z "$name" ]] && continue
|
||||||
[[ -n "$bm_name" && -n "$bm_path" ]] && printf "%s\t%s\t%s\t%s\n" "$ICON_DIR" "bookmark" "$bm_name" "cd $bm_path"
|
printf "%s\t%s\t%s\t%s\n" "$ICON_DIR" "bookmark" "$name" "$path"
|
||||||
done < "$PALETTE_BOOKMARKS_FILE"
|
done < "$PALETTE_BOOKMARKS_FILE"
|
||||||
}
|
}
|
||||||
|
|
||||||
_palette_get_scripts() {
|
|
||||||
[[ ! -d "$DOTFILES_DIR/bin" ]] && return
|
|
||||||
|
|
||||||
for script in "$DOTFILES_DIR/bin"/*.sh; do
|
|
||||||
[[ -f "$script" ]] || continue
|
|
||||||
local script_name=$(basename "$script" .sh)
|
|
||||||
printf "%s\t%s\t%s\t%s\n" "$ICON_SCRIPT" "script" "$script_name" "$script"
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
_palette_get_git_commands() {
|
|
||||||
git rev-parse --git-dir &>/dev/null || return
|
|
||||||
|
|
||||||
local branch=$(git branch --show-current 2>/dev/null)
|
|
||||||
|
|
||||||
printf "%s\t%s\t%s\t%s\n" "$ICON_GIT" "git" "status" "git status"
|
|
||||||
printf "%s\t%s\t%s\t%s\n" "$ICON_GIT" "git" "pull $branch" "git pull origin $branch"
|
|
||||||
printf "%s\t%s\t%s\t%s\n" "$ICON_GIT" "git" "push $branch" "git push origin $branch"
|
|
||||||
printf "%s\t%s\t%s\t%s\n" "$ICON_GIT" "git" "diff" "git diff"
|
|
||||||
printf "%s\t%s\t%s\t%s\n" "$ICON_GIT" "git" "log" "git log --oneline -20"
|
|
||||||
printf "%s\t%s\t%s\t%s\n" "$ICON_GIT" "git" "stash" "git stash"
|
|
||||||
printf "%s\t%s\t%s\t%s\n" "$ICON_GIT" "git" "stash pop" "git stash pop"
|
|
||||||
}
|
|
||||||
|
|
||||||
_palette_get_docker_commands() {
|
|
||||||
command -v docker &>/dev/null || return
|
|
||||||
|
|
||||||
printf "%s\t%s\t%s\t%s\n" "$ICON_DOCKER" "docker" "ps" "docker ps"
|
|
||||||
printf "%s\t%s\t%s\t%s\n" "$ICON_DOCKER" "docker" "ps -a" "docker ps -a"
|
|
||||||
printf "%s\t%s\t%s\t%s\n" "$ICON_DOCKER" "docker" "images" "docker images"
|
|
||||||
printf "%s\t%s\t%s\t%s\n" "$ICON_DOCKER" "docker" "compose up" "docker-compose up -d"
|
|
||||||
printf "%s\t%s\t%s\t%s\n" "$ICON_DOCKER" "docker" "compose down" "docker-compose down"
|
|
||||||
printf "%s\t%s\t%s\t%s\n" "$ICON_DOCKER" "docker" "prune" "docker system prune -af"
|
|
||||||
}
|
|
||||||
|
|
||||||
_palette_get_actions() {
|
_palette_get_actions() {
|
||||||
printf "%s\t%s\t%s\t%s\n" "$ICON_ACTION" "action" "Reload shell" "exec zsh"
|
cat << 'EOF'
|
||||||
printf "%s\t%s\t%s\t%s\n" "$ICON_EDIT" "action" "Edit .zshrc" "${EDITOR:-vim} ~/.zshrc"
|
★ action reload-shell Reload zsh configuration
|
||||||
printf "%s\t%s\t%s\t%s\n" "$ICON_EDIT" "action" "Edit dotfiles.conf" "${EDITOR:-vim} $DOTFILES_DIR/dotfiles.conf"
|
★ action edit-zshrc Edit ~/.zshrc
|
||||||
printf "%s\t%s\t%s\t%s\n" "$ICON_EDIT" "action" "Edit theme" "${EDITOR:-vim} $DOTFILES_DIR/zsh/themes/adlee.zsh-theme"
|
★ action dotfiles-update Update dotfiles
|
||||||
printf "%s\t%s\t%s\t%s\n" "$ICON_SCRIPT" "action" "Dotfiles doctor" "dfd"
|
EOF
|
||||||
printf "%s\t%s\t%s\t%s\n" "$ICON_SCRIPT" "action" "Dotfiles sync" "dfs"
|
df_in_git_repo && cat << 'EOF'
|
||||||
printf "%s\t%s\t%s\t%s\n" "$ICON_SCRIPT" "action" "Shell stats" "dfstats"
|
⎇ git git-status Show git status
|
||||||
printf "%s\t%s\t%s\t%s\n" "$ICON_SCRIPT" "action" "Compile zsh" "dfcompile"
|
⎇ git git-pull Pull latest
|
||||||
printf "%s\t%s\t%s\t%s\n" "$ICON_SCRIPT" "action" "Vault list" "vault list"
|
⎇ git git-push Push commits
|
||||||
printf "%s\t%s\t%s\t%s\n" "$ICON_ACTION" "action" "Clear screen" "clear"
|
EOF
|
||||||
printf "%s\t%s\t%s\t%s\n" "$ICON_DIR" "action" "Home" "cd ~"
|
|
||||||
printf "%s\t%s\t%s\t%s\n" "$ICON_DIR" "action" "Dotfiles" "cd $DOTFILES_DIR"
|
|
||||||
printf "%s\t%s\t%s\t%s\n" "$ICON_DIR" "action" "Projects" "cd ~/projects 2>/dev/null || cd ~"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_palette_get_directories() {
|
_palette_run_action() {
|
||||||
dirs -v 2>/dev/null | tail -n +2 | head -10 | while read -r num dir; do
|
case "$1" in
|
||||||
[[ -n "$dir" ]] && printf "%s\t%s\t%s\t%s\n" "$ICON_DIR" "recent" "$dir" "cd $dir"
|
reload-shell) source ~/.zshrc; df_print_success "Shell reloaded" ;;
|
||||||
done
|
edit-zshrc) ${EDITOR:-vim} ~/.zshrc ;;
|
||||||
}
|
dotfiles-update) cd "$DOTFILES_DIR" && git pull ;;
|
||||||
|
git-status) git status ;;
|
||||||
# ============================================================================
|
git-pull) git pull ;;
|
||||||
# Main Palette Function
|
git-push) git push ;;
|
||||||
# ============================================================================
|
*) df_print_error "Unknown action: $1" ;;
|
||||||
|
|
||||||
_palette_generate_entries() {
|
|
||||||
_palette_get_actions
|
|
||||||
_palette_get_git_commands
|
|
||||||
_palette_get_docker_commands
|
|
||||||
_palette_get_aliases
|
|
||||||
_palette_get_bookmarks
|
|
||||||
_palette_get_scripts
|
|
||||||
_palette_get_directories
|
|
||||||
_palette_get_history
|
|
||||||
_palette_get_functions
|
|
||||||
}
|
|
||||||
|
|
||||||
command_palette() {
|
|
||||||
_palette_check_deps || return 1
|
|
||||||
|
|
||||||
local selection
|
|
||||||
selection=$(_palette_generate_entries | \
|
|
||||||
fzf --height=60% \
|
|
||||||
--layout=reverse \
|
|
||||||
--border=rounded \
|
|
||||||
--prompt='❯ ' \
|
|
||||||
--pointer='▶' \
|
|
||||||
--header='Command Palette (ESC to cancel)' \
|
|
||||||
--preview-window=hidden \
|
|
||||||
--delimiter=$'\t' \
|
|
||||||
--with-nth=1,3 \
|
|
||||||
--tabstop=2 \
|
|
||||||
--ansi \
|
|
||||||
--bind='ctrl-r:reload(_palette_generate_entries)' \
|
|
||||||
--expect=ctrl-e,ctrl-y)
|
|
||||||
|
|
||||||
[[ -z "$selection" ]] && return
|
|
||||||
|
|
||||||
local key=$(echo "$selection" | head -1)
|
|
||||||
local line=$(echo "$selection" | tail -1)
|
|
||||||
local cmd=$(echo "$line" | cut -f4)
|
|
||||||
|
|
||||||
[[ -z "$cmd" ]] && return
|
|
||||||
|
|
||||||
case "$key" in
|
|
||||||
ctrl-e)
|
|
||||||
print -z "$cmd"
|
|
||||||
;;
|
|
||||||
ctrl-y)
|
|
||||||
echo -n "$cmd" | pbcopy 2>/dev/null || echo -n "$cmd" | xclip -selection clipboard 2>/dev/null
|
|
||||||
echo "Copied: $cmd"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "❯ $cmd"
|
|
||||||
eval "$cmd"
|
|
||||||
;;
|
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
# Alias for easier access
|
palette() {
|
||||||
palette() { command_palette; }
|
df_require_cmd fzf || return 1
|
||||||
p() { command_palette; }
|
local items=$(_palette_get_actions; _palette_get_aliases; _palette_get_functions; _palette_get_bookmarks; _palette_get_history)
|
||||||
|
local sel=$(echo "$items" | fzf --ansi --delimiter='\t' --with-nth=1,3,4 $(df_fzf_opts) --prompt='> ')
|
||||||
# ============================================================================
|
[[ -z "$sel" ]] && return
|
||||||
# Bookmark Management
|
local type=$(echo "$sel" | cut -f2) name=$(echo "$sel" | cut -f3) detail=$(echo "$sel" | cut -f4)
|
||||||
# ============================================================================
|
case "$type" in
|
||||||
|
alias|history) print -z "$name" ;;
|
||||||
|
function) print -z "$name " ;;
|
||||||
|
bookmark) cd "$detail" && pwd ;;
|
||||||
|
action|git) _palette_run_action "$name" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
bookmark() {
|
bookmark() {
|
||||||
local bm_name="$1"
|
local cmd="${1:-list}"; shift 2>/dev/null
|
||||||
local bm_path="${2:-$(pwd)}"
|
case "$cmd" in
|
||||||
|
add)
|
||||||
# Ensure bookmarks file parent directory exists
|
local name="$1" path="${2:-$(pwd)}" desc="$3"
|
||||||
mkdir -p "$(dirname "$PALETTE_BOOKMARKS_FILE")" 2>/dev/null
|
[[ -z "$name" ]] && { echo "Usage: bookmark add <name> [path]"; return 1; }
|
||||||
|
df_ensure_file "$PALETTE_BOOKMARKS_FILE" "# Bookmarks: name|path|description"
|
||||||
# Create bookmarks file if it doesn't exist
|
grep -q "^${name}|" "$PALETTE_BOOKMARKS_FILE" 2>/dev/null && {
|
||||||
[[ ! -f "$PALETTE_BOOKMARKS_FILE" ]] && touch "$PALETTE_BOOKMARKS_FILE"
|
df_confirm "Overwrite '$name'?" || return 1
|
||||||
|
grep -v "^${name}|" "$PALETTE_BOOKMARKS_FILE" > "${PALETTE_BOOKMARKS_FILE}.tmp"
|
||||||
if [[ -z "$bm_name" ]]; then
|
mv "${PALETTE_BOOKMARKS_FILE}.tmp" "$PALETTE_BOOKMARKS_FILE"
|
||||||
echo "Usage: bookmark <name> [path]"
|
}
|
||||||
echo " bookmark list"
|
echo "${name}|${path}|${desc}" >> "$PALETTE_BOOKMARKS_FILE"
|
||||||
echo " bookmark delete <name>"
|
df_print_success "Bookmarked: $name → $path"
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
case "$bm_name" in
|
|
||||||
list|ls)
|
|
||||||
df_print_func_name "Bookmarks"
|
|
||||||
if [[ -s "$PALETTE_BOOKMARKS_FILE" ]]; then
|
|
||||||
while IFS='|' read -r stored_name stored_path || [[ -n "$stored_name" ]]; do
|
|
||||||
[[ -n "$stored_name" ]] && echo -e " ${DF_GREEN}●${DF_NC} $stored_name → $stored_path"
|
|
||||||
done < "$PALETTE_BOOKMARKS_FILE"
|
|
||||||
else
|
|
||||||
echo "No bookmarks yet"
|
|
||||||
fi
|
|
||||||
;;
|
;;
|
||||||
delete|rm)
|
delete|rm)
|
||||||
local to_delete="$2"
|
[[ -z "$1" ]] && { echo "Usage: bookmark delete <name>"; return 1; }
|
||||||
if [[ -z "$to_delete" ]]; then
|
grep -q "^${1}|" "$PALETTE_BOOKMARKS_FILE" 2>/dev/null || { df_print_error "Not found: $1"; return 1; }
|
||||||
echo "Specify bookmark to delete"
|
grep -v "^${1}|" "$PALETTE_BOOKMARKS_FILE" > "${PALETTE_BOOKMARKS_FILE}.tmp"
|
||||||
return 1
|
mv "${PALETTE_BOOKMARKS_FILE}.tmp" "$PALETTE_BOOKMARKS_FILE"
|
||||||
fi
|
df_print_success "Deleted: $1"
|
||||||
if [[ -s "$PALETTE_BOOKMARKS_FILE" ]]; then
|
|
||||||
# Use a temp file approach that won't hang
|
|
||||||
local temp_file="${PALETTE_BOOKMARKS_FILE}.tmp.$$"
|
|
||||||
grep -v "^${to_delete}|" "$PALETTE_BOOKMARKS_FILE" > "$temp_file" 2>/dev/null || true
|
|
||||||
mv -f "$temp_file" "$PALETTE_BOOKMARKS_FILE"
|
|
||||||
echo -e "${DF_GREEN}✓${DF_NC} Deleted: $to_delete"
|
|
||||||
else
|
|
||||||
echo "No bookmarks to delete"
|
|
||||||
fi
|
|
||||||
;;
|
;;
|
||||||
*)
|
list|ls)
|
||||||
# Remove existing bookmark with same name (if file has content)
|
df_print_func_name "Bookmarks"
|
||||||
if [[ -s "$PALETTE_BOOKMARKS_FILE" ]]; then
|
[[ ! -f "$PALETTE_BOOKMARKS_FILE" ]] && { df_print_info "No bookmarks"; return; }
|
||||||
local temp_file="${PALETTE_BOOKMARKS_FILE}.tmp.$$"
|
while IFS='|' read -r name path desc; do
|
||||||
grep -v "^${bm_name}|" "$PALETTE_BOOKMARKS_FILE" > "$temp_file" 2>/dev/null || true
|
[[ "$name" =~ ^# || -z "$name" ]] && continue
|
||||||
mv -f "$temp_file" "$PALETTE_BOOKMARKS_FILE"
|
df_print_indent "● $name → $path"
|
||||||
fi
|
done < "$PALETTE_BOOKMARKS_FILE"
|
||||||
# Add new bookmark
|
|
||||||
echo "${bm_name}|${bm_path}" >> "$PALETTE_BOOKMARKS_FILE"
|
|
||||||
echo -e "${DF_GREEN}✓${DF_NC} Bookmarked: $bm_name → $bm_path"
|
|
||||||
;;
|
;;
|
||||||
|
go)
|
||||||
|
[[ -z "$1" ]] && { echo "Usage: bookmark go <name>"; return 1; }
|
||||||
|
local path=$(grep "^${1}|" "$PALETTE_BOOKMARKS_FILE" 2>/dev/null | cut -d'|' -f2)
|
||||||
|
[[ -n "$path" ]] && cd "$path" || df_print_error "Not found: $1"
|
||||||
|
;;
|
||||||
|
*) echo "Usage: bookmark <add|delete|list|go>" ;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
# Quick jump to bookmark
|
bm() {
|
||||||
jump() {
|
df_require_cmd fzf || return 1
|
||||||
local bm_name="$1"
|
[[ ! -f "$PALETTE_BOOKMARKS_FILE" ]] && { df_print_info "No bookmarks"; return 1; }
|
||||||
|
local sel=$(grep -v "^#" "$PALETTE_BOOKMARKS_FILE" | grep -v "^$" | \
|
||||||
if [[ -z "$bm_name" ]]; then
|
fzf $(df_fzf_opts) --delimiter='|' --with-nth=1,2 --prompt='Bookmark > ')
|
||||||
# Fuzzy select bookmark
|
[[ -n "$sel" ]] && cd "$(echo "$sel" | cut -d'|' -f2)"
|
||||||
if [[ ! -s "$PALETTE_BOOKMARKS_FILE" ]]; then
|
|
||||||
echo "No bookmarks"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
local selection=$(cat "$PALETTE_BOOKMARKS_FILE" | \
|
|
||||||
fzf --height=40% --layout=reverse --delimiter='|' --with-nth=1 \
|
|
||||||
--preview='echo "Path: $(echo {} | cut -d"|" -f2)"')
|
|
||||||
|
|
||||||
if [[ -n "$selection" ]]; then
|
|
||||||
local jump_path=$(echo "$selection" | cut -d'|' -f2)
|
|
||||||
cd "$jump_path" && echo "→ $jump_path"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
# Direct jump
|
|
||||||
local jump_path=$(grep "^${bm_name}|" "$PALETTE_BOOKMARKS_FILE" 2>/dev/null | cut -d'|' -f2)
|
|
||||||
if [[ -n "$jump_path" ]]; then
|
|
||||||
cd "$jump_path" && echo "→ $jump_path"
|
|
||||||
else
|
|
||||||
echo "Bookmark not found: $bm_name"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Aliases
|
_palette_widget() { BUFFER=""; zle redisplay; palette; zle reset-prompt; }
|
||||||
bm() { bookmark "$@"; }
|
|
||||||
j() { jump "$@"; }
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Widget for Keybinding
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
_palette_widget() {
|
|
||||||
command_palette
|
|
||||||
zle reset-prompt
|
|
||||||
}
|
|
||||||
|
|
||||||
# Register widget
|
|
||||||
zle -N _palette_widget
|
zle -N _palette_widget
|
||||||
|
|
||||||
# Bind to Ctrl+Space (^@)
|
|
||||||
bindkey "$PALETTE_HOTKEY" _palette_widget
|
bindkey "$PALETTE_HOTKEY" _palette_widget
|
||||||
|
|
||||||
# Alternative binding: Ctrl+P
|
alias p='palette' bml='bookmark list' bma='bookmark add' bmg='bookmark go'
|
||||||
bindkey '^P' _palette_widget
|
|
||||||
|
|||||||
@@ -1,198 +1,84 @@
|
|||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Password Manager Integration for Zsh (LastPass Only)
|
# Password Manager Integration (LastPass CLI)
|
||||||
# ============================================================================
|
|
||||||
# Unified interface for LastPass CLI
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# pw list # List all items
|
|
||||||
# pw get <item> # Get password
|
|
||||||
# pw otp <item> # Get OTP/TOTP code
|
|
||||||
# pw search <query> # Search items
|
|
||||||
# pw copy <item> # Copy password to clipboard
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
# Source shared colors (with fallback)
|
source "${0:A:h}/../lib/utils.zsh" 2>/dev/null || \
|
||||||
source "${0:A:h}/../lib/colors.zsh" 2>/dev/null || \
|
source "$HOME/.dotfiles/zsh/lib/utils.zsh" 2>/dev/null
|
||||||
source "$HOME/.dotfiles/zsh/lib/colors.zsh" 2>/dev/null || {
|
|
||||||
typeset -g DF_GREEN=$'\033[0;32m' DF_BLUE=$'\033[0;34m'
|
|
||||||
typeset -g DF_YELLOW=$'\033[1;33m' DF_CYAN=$'\033[0;36m'
|
|
||||||
typeset -g DF_RED=$'\033[0;31m' DF_NC=$'\033[0m'
|
|
||||||
}
|
|
||||||
|
|
||||||
# ============================================================================
|
typeset -g PW_CLIP_TIME="${PW_CLIP_TIME:-45}"
|
||||||
# LastPass Functions
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
_lp_ensure_session() {
|
_pw_check() {
|
||||||
|
df_require_cmd lpass lastpass-cli || return 1
|
||||||
if ! lpass status -q 2>/dev/null; then
|
if ! lpass status -q 2>/dev/null; then
|
||||||
echo "Signing into LastPass..." >&2
|
df_print_warning "Not logged in"
|
||||||
lpass login "${LASTPASS_EMAIL:-}"
|
df_print_step "Logging in..."
|
||||||
|
lpass login --trust "${LPASS_USER:-}" || { df_print_error "Login failed"; return 1; }
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
_lp_list() {
|
_pw_copy() {
|
||||||
_lp_ensure_session
|
local text="$1" label="${2:-Password}"
|
||||||
lpass ls --format="%an\t%ag" 2>/dev/null
|
if df_cmd_exists wl-copy; then
|
||||||
}
|
echo -n "$text" | wl-copy
|
||||||
|
elif df_cmd_exists xclip; then
|
||||||
_lp_get() {
|
echo -n "$text" | xclip -selection clipboard
|
||||||
local item="$1"
|
else
|
||||||
local field="${2:-password}"
|
df_print_error "No clipboard tool (install wl-clipboard or xclip)"
|
||||||
_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"
|
|
||||||
return 1
|
return 1
|
||||||
fi
|
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
|
case "$cmd" in
|
||||||
list|ls|l)
|
login) lpass login --trust "${LPASS_USER:-}" ;;
|
||||||
df_print_func_name "LastPass Vault"
|
logout) lpass logout -f; df_print_success "Logged out" ;;
|
||||||
_lp_list
|
sync) _pw_check || return 1; df_print_step "Syncing..."; lpass sync; df_print_success "Synced" ;;
|
||||||
|
show) _pw_check || return 1; [[ -z "$2" ]] && { echo "Usage: pw show <entry>"; return 1; }; lpass show "$2" ;;
|
||||||
|
gen|generate)
|
||||||
|
local len="${2:-20}"
|
||||||
|
local pass=$(tr -dc 'A-Za-z0-9!@#$%^&*' < /dev/urandom | head -c "$len")
|
||||||
|
_pw_copy "$pass" "Generated password"
|
||||||
;;
|
;;
|
||||||
|
list|ls) _pw_check || return 1; df_print_func_name "Password Entries"; lpass ls --long ;;
|
||||||
get|g|show)
|
search|*)
|
||||||
local item="$1"
|
_pw_check || return 1
|
||||||
local field="${2:-password}"
|
local query="$1"; [[ "$cmd" == "search" ]] && query="$2"
|
||||||
[[ -z "$item" ]] && { echo "Usage: pw get <item> [field]"; return 1; }
|
if df_cmd_exists fzf && [[ -z "$query" ]]; then
|
||||||
_lp_get "$item" "$field"
|
local entry=$(lpass ls --format "%an (%au) [%ai]" 2>/dev/null | fzf $(df_fzf_opts) --prompt='Password > ')
|
||||||
;;
|
[[ -z "$entry" ]] && return
|
||||||
|
local id=$(echo "$entry" | grep -oP '\[\K[^\]]+(?=\]$)')
|
||||||
otp|totp|2fa)
|
local pass=$(lpass show --password "$id" 2>/dev/null)
|
||||||
local item="$1"
|
[[ -n "$pass" ]] && _pw_copy "$pass" || df_print_error "Could not retrieve"
|
||||||
[[ -z "$item" ]] && { echo "Usage: pw otp <item>"; return 1; }
|
|
||||||
_lp_otp "$item"
|
|
||||||
;;
|
|
||||||
|
|
||||||
search|find|s)
|
|
||||||
local query="$1"
|
|
||||||
[[ -z "$query" ]] && { echo "Usage: pw search <query>"; return 1; }
|
|
||||||
df_print_func_name "LastPass Search: $query"
|
|
||||||
_lp_search "$query"
|
|
||||||
;;
|
|
||||||
|
|
||||||
copy|cp|c)
|
|
||||||
local item="$1"
|
|
||||||
local field="${2:-password}"
|
|
||||||
[[ -z "$item" ]] && { echo "Usage: pw copy <item> [field]"; return 1; }
|
|
||||||
|
|
||||||
local value=$(_lp_get "$item" "$field")
|
|
||||||
|
|
||||||
if [[ -n "$value" ]]; then
|
|
||||||
echo -n "$value" | xclip -selection clipboard 2>/dev/null || \
|
|
||||||
echo -n "$value" | xsel --clipboard 2>/dev/null || \
|
|
||||||
{ echo "Could not copy to clipboard"; return 1; }
|
|
||||||
echo -e "${DF_GREEN}✓${DF_NC} Copied to clipboard"
|
|
||||||
else
|
else
|
||||||
echo -e "${DF_RED}✗${DF_NC} Item not found or empty"
|
[[ -z "$query" ]] && { echo "Usage: pw <search-term>"; return 1; }
|
||||||
return 1
|
local results=$(lpass ls --format "%an [%ai]" 2>/dev/null | grep -i "$query")
|
||||||
|
local count=$(echo "$results" | grep -c . 2>/dev/null || echo 0)
|
||||||
|
if (( count == 0 )); then
|
||||||
|
df_print_warning "No entries for: $query"
|
||||||
|
elif (( count == 1 )); then
|
||||||
|
local id=$(echo "$results" | grep -oP '\[\K[^\]]+(?=\]$)')
|
||||||
|
_pw_copy "$(lpass show --password "$id" 2>/dev/null)"
|
||||||
|
else
|
||||||
|
df_print_warning "Multiple entries:"
|
||||||
|
echo "$results" | while read -r l; do df_print_indent "$l"; done
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
|
help|--help|-h)
|
||||||
lock)
|
df_print_func_name "Password Manager"
|
||||||
lpass logout -f 2>/dev/null
|
cat << 'EOF'
|
||||||
echo -e "${DF_GREEN}✓${DF_NC} Logged out of LastPass"
|
pw <search> Search and copy password
|
||||||
;;
|
pw show <n> Show entry details
|
||||||
|
pw list List all entries
|
||||||
help|--help|-h|*)
|
pw gen [len] Generate password (default: 20)
|
||||||
df_print_func_name "Password Manager CLI"
|
pw sync Sync vault
|
||||||
echo "Usage: pw <command> [args]"
|
pw login/logout Auth commands
|
||||||
echo
|
EOF
|
||||||
echo "Commands:"
|
|
||||||
echo " list List all items"
|
|
||||||
echo " get <item> [field] Get field (default: password)"
|
|
||||||
echo " otp <item> Get OTP/TOTP code"
|
|
||||||
echo " search <query> Search items"
|
|
||||||
echo " copy <item> [field] Copy to clipboard"
|
|
||||||
echo " lock Logout/lock session"
|
|
||||||
echo " help Show this help"
|
|
||||||
echo
|
|
||||||
echo "Fields: password, username, url, notes, or custom field name"
|
|
||||||
echo
|
|
||||||
echo "Examples:"
|
|
||||||
echo " pw get github"
|
|
||||||
echo " pw get github username"
|
|
||||||
echo " pw otp github"
|
|
||||||
echo " pw copy aws"
|
|
||||||
echo " pw search mail"
|
|
||||||
echo
|
|
||||||
echo "Install: yay -S lastpass-cli"
|
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================================================
|
alias pwc='pw' pws='pw show' pwg='pw gen' pwl='pw list'
|
||||||
# Aliases
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
alias pwl='pw list'
|
|
||||||
alias pwg='pw get'
|
|
||||||
alias pwc='pw copy'
|
|
||||||
alias pws='pw search'
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# FZF Integration (if available)
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
if command -v fzf &>/dev/null; then
|
|
||||||
pwf() {
|
|
||||||
if ! command -v lpass &>/dev/null; then
|
|
||||||
echo "LastPass CLI not installed"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
local item=$(_lp_list | fzf --height=40% --reverse | cut -f1)
|
|
||||||
|
|
||||||
if [[ -n "$item" ]]; then
|
|
||||||
pw copy "$item"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
pwof() {
|
|
||||||
if ! command -v lpass &>/dev/null; then
|
|
||||||
echo "LastPass CLI not installed"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
local item=$(_lp_list | fzf --height=40% --reverse | cut -f1)
|
|
||||||
|
|
||||||
if [[ -n "$item" ]]; then
|
|
||||||
local otp=$(pw otp "$item")
|
|
||||||
if [[ -n "$otp" ]]; then
|
|
||||||
echo -n "$otp" | xclip -selection clipboard 2>/dev/null || \
|
|
||||||
echo -n "$otp" | xsel --clipboard 2>/dev/null
|
|
||||||
echo -e "${DF_GREEN}✓${DF_NC} OTP copied: $otp"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
fi
|
|
||||||
|
|||||||
@@ -1,488 +1,173 @@
|
|||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Python Project Template Functions
|
# Python Project Template Functions
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Quick project scaffolding with virtual environments
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# py-new <project_name> # Create new Python project
|
|
||||||
# py-django <project_name> # Create Django project
|
|
||||||
# py-flask <project_name> # Create Flask project
|
|
||||||
# py-fastapi <project_name> # Create FastAPI project
|
|
||||||
# py-data <project_name> # Create data science project
|
|
||||||
# py-cli <project_name> # Create CLI tool project
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
# Source shared colors (with fallback)
|
source "${0:A:h}/../lib/utils.zsh" 2>/dev/null || \
|
||||||
source "${0:A:h}/../lib/colors.zsh" 2>/dev/null || \
|
source "$HOME/.dotfiles/zsh/lib/utils.zsh" 2>/dev/null
|
||||||
source "$HOME/.dotfiles/zsh/lib/colors.zsh" 2>/dev/null || {
|
|
||||||
typeset -g DF_GREEN=$'\033[0;32m' DF_BLUE=$'\033[0;34m'
|
|
||||||
typeset -g DF_YELLOW=$'\033[1;33m' DF_CYAN=$'\033[0;36m'
|
|
||||||
typeset -g DF_RED=$'\033[0;31m' DF_NC=$'\033[0m'
|
|
||||||
}
|
|
||||||
|
|
||||||
# ============================================================================
|
typeset -g PY_PYTHON="${PY_PYTHON:-python3}"
|
||||||
# Configuration
|
typeset -g PY_VENV="${PY_VENV:-venv}"
|
||||||
# ============================================================================
|
typeset -g PY_GIT_INIT="${PY_GIT_INIT:-true}"
|
||||||
|
|
||||||
typeset -g PY_TEMPLATE_BASE_DIR="${PY_TEMPLATE_BASE_DIR:-$HOME/projects}"
|
_py_check_name() {
|
||||||
typeset -g PY_TEMPLATE_PYTHON="${PY_TEMPLATE_PYTHON:-python3}"
|
[[ -z "$1" ]] && { df_print_warning "Project name required"; return 1; }
|
||||||
typeset -g PY_TEMPLATE_VENV_NAME="${PY_TEMPLATE_VENV_NAME:-venv}"
|
[[ -d "$1" ]] && { df_print_warning "Directory '$1' exists"; return 1; }
|
||||||
typeset -g PY_TEMPLATE_USE_POETRY="${PY_TEMPLATE_USE_POETRY:-false}"
|
|
||||||
typeset -g PY_TEMPLATE_GIT_INIT="${PY_TEMPLATE_GIT_INIT:-true}"
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Helper Functions
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
_py_print_step() {
|
|
||||||
echo -e "${DF_BLUE}==>${DF_NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
_py_print_success() {
|
|
||||||
echo -e "${DF_GREEN}✓${DF_NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
_py_print_info() {
|
|
||||||
echo -e "${DF_CYAN}ℹ${DF_NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
_py_check_project_name() {
|
|
||||||
local name="$1"
|
|
||||||
if [[ -z "$name" ]]; then
|
|
||||||
echo -e "${DF_YELLOW}⚠${DF_NC} Project name required"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
if [[ -d "$name" ]]; then
|
|
||||||
echo -e "${DF_YELLOW}⚠${DF_NC} Directory '$name' already exists"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
_py_create_venv() {
|
_py_venv() {
|
||||||
local project_dir="$1"
|
df_print_step "Creating virtual environment"
|
||||||
_py_print_step "Creating virtual environment"
|
"$PY_PYTHON" -m venv "$1/$PY_VENV"
|
||||||
|
df_print_success "Created: $PY_VENV"
|
||||||
if [[ "$PY_TEMPLATE_USE_POETRY" == "true" ]] && command -v poetry &>/dev/null; then
|
|
||||||
cd "$project_dir"
|
|
||||||
poetry init --no-interaction
|
|
||||||
poetry env use "$PY_TEMPLATE_PYTHON"
|
|
||||||
_py_print_success "Poetry environment created"
|
|
||||||
else
|
|
||||||
"$PY_TEMPLATE_PYTHON" -m venv "$project_dir/$PY_TEMPLATE_VENV_NAME"
|
|
||||||
_py_print_success "Virtual environment created: $PY_TEMPLATE_VENV_NAME"
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_py_create_gitignore() {
|
_py_gitignore() {
|
||||||
local project_dir="$1"
|
cat > "$1/.gitignore" << 'EOF'
|
||||||
cat > "$project_dir/.gitignore" << 'EOF'
|
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
*$py.class
|
|
||||||
*.so
|
*.so
|
||||||
.Python
|
|
||||||
build/
|
build/
|
||||||
dist/
|
dist/
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
venv/
|
venv/
|
||||||
env/
|
.venv/
|
||||||
.venv
|
|
||||||
.vscode/
|
|
||||||
.idea/
|
|
||||||
*.swp
|
|
||||||
.pytest_cache/
|
|
||||||
.coverage
|
|
||||||
htmlcov/
|
|
||||||
.env
|
.env
|
||||||
*.log
|
*.log
|
||||||
*.db
|
.pytest_cache/
|
||||||
*.sqlite3
|
|
||||||
.mypy_cache/
|
.mypy_cache/
|
||||||
EOF
|
EOF
|
||||||
_py_print_success "Created .gitignore"
|
df_print_success "Created .gitignore"
|
||||||
}
|
}
|
||||||
|
|
||||||
_py_init_git() {
|
_py_git() {
|
||||||
local project_dir="$1"
|
[[ "$PY_GIT_INIT" == "true" ]] && { cd "$1"; git init; git add .; git commit -m "Initial commit"; df_print_success "Git initialized"; }
|
||||||
if [[ "$PY_TEMPLATE_GIT_INIT" == "true" ]]; then
|
|
||||||
cd "$project_dir"
|
|
||||||
git init
|
|
||||||
git add .
|
|
||||||
git commit -m "Initial commit: project scaffolding"
|
|
||||||
_py_print_success "Git repository initialized"
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_py_show_next_steps() {
|
_py_next() {
|
||||||
local project_name="$1"
|
echo ""
|
||||||
local has_venv="$2"
|
df_print_section "Next steps"
|
||||||
echo
|
df_print_indent "cd $1"
|
||||||
echo -e "${DF_CYAN}Next steps:${DF_NC}"
|
df_print_indent "source $PY_VENV/bin/activate"
|
||||||
echo " cd $project_name"
|
|
||||||
if [[ "$has_venv" == "true" ]]; then
|
|
||||||
if [[ "$PY_TEMPLATE_USE_POETRY" == "true" ]]; then
|
|
||||||
echo " poetry shell"
|
|
||||||
else
|
|
||||||
echo " source $PY_TEMPLATE_VENV_NAME/bin/activate"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
echo " # Start coding!"
|
|
||||||
echo
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Base Python Project Template
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
py-new() {
|
py-new() {
|
||||||
local project_name="$1"
|
_py_check_name "$1" || return 1
|
||||||
_py_check_project_name "$project_name" || return 1
|
df_print_func_name "Python Project: $1"
|
||||||
|
mkdir -p "$1"/{src,tests}
|
||||||
df_print_func_name "Python Project: $project_name"
|
touch "$1/src/__init__.py" "$1/tests/__init__.py"
|
||||||
|
cat > "$1/src/main.py" << 'EOF'
|
||||||
_py_print_step "Creating project structure"
|
|
||||||
mkdir -p "$project_name"/{src,tests,docs}
|
|
||||||
touch "$project_name/src/__init__.py"
|
|
||||||
touch "$project_name/tests/__init__.py"
|
|
||||||
|
|
||||||
cat > "$project_name/src/main.py" << 'EOF'
|
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Main module."""
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
print("Hello from Python!")
|
print("Hello!")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
EOF
|
EOF
|
||||||
|
echo "# Dependencies" > "$1/requirements.txt"
|
||||||
cat > "$project_name/requirements.txt" << 'EOF'
|
_py_venv "$1"; _py_gitignore "$1"; _py_git "$1"
|
||||||
# Production dependencies
|
df_print_success "Created: $1"
|
||||||
# Development: pytest, black, flake8, mypy
|
_py_next "$1"
|
||||||
EOF
|
|
||||||
|
|
||||||
_py_print_success "Project structure created"
|
|
||||||
_py_create_venv "$project_name"
|
|
||||||
_py_create_gitignore "$project_name"
|
|
||||||
_py_init_git "$project_name"
|
|
||||||
|
|
||||||
echo
|
|
||||||
_py_print_success "Project '$project_name' created successfully!"
|
|
||||||
_py_show_next_steps "$project_name" "true"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Django Project Template
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
py-django() {
|
|
||||||
local project_name="$1"
|
|
||||||
_py_check_project_name "$project_name" || return 1
|
|
||||||
|
|
||||||
df_print_func_name "Django Project: $project_name"
|
|
||||||
|
|
||||||
mkdir -p "$project_name"
|
|
||||||
_py_create_venv "$project_name"
|
|
||||||
|
|
||||||
_py_print_step "Installing Django"
|
|
||||||
cd "$project_name"
|
|
||||||
"$PY_TEMPLATE_VENV_NAME/bin/pip" install django
|
|
||||||
|
|
||||||
_py_print_step "Creating Django project structure"
|
|
||||||
"$PY_TEMPLATE_VENV_NAME/bin/django-admin" startproject config .
|
|
||||||
|
|
||||||
cat > "requirements.txt" << 'EOF'
|
|
||||||
Django>=4.2.0
|
|
||||||
python-decouple>=3.8
|
|
||||||
EOF
|
|
||||||
|
|
||||||
mkdir -p apps static templates media
|
|
||||||
_py_create_gitignore "."
|
|
||||||
_py_init_git "."
|
|
||||||
cd ..
|
|
||||||
|
|
||||||
echo
|
|
||||||
_py_print_success "Django project '$project_name' created!"
|
|
||||||
_py_show_next_steps "$project_name" "true"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Flask Project Template
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
py-flask() {
|
py-flask() {
|
||||||
local project_name="$1"
|
_py_check_name "$1" || return 1
|
||||||
_py_check_project_name "$project_name" || return 1
|
df_print_func_name "Flask Project: $1"
|
||||||
|
mkdir -p "$1"/{app/{templates,static},tests}
|
||||||
df_print_func_name "Flask Project: $project_name"
|
_py_venv "$1"
|
||||||
|
df_print_step "Installing Flask"
|
||||||
mkdir -p "$project_name"/{app/{templates,static/{css,js}},tests}
|
"$1/$PY_VENV/bin/pip" install flask -q
|
||||||
_py_create_venv "$project_name"
|
cat > "$1/app/__init__.py" << 'EOF'
|
||||||
|
|
||||||
cd "$project_name"
|
|
||||||
_py_print_step "Installing Flask"
|
|
||||||
"$PY_TEMPLATE_VENV_NAME/bin/pip" install flask
|
|
||||||
|
|
||||||
cat > "app/__init__.py" << 'EOF'
|
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
|
def create_app():
|
||||||
def create_app(config=None):
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
if config:
|
|
||||||
app.config.from_object(config)
|
|
||||||
from app.routes import main
|
from app.routes import main
|
||||||
app.register_blueprint(main)
|
app.register_blueprint(main)
|
||||||
return app
|
return app
|
||||||
EOF
|
EOF
|
||||||
|
cat > "$1/app/routes.py" << 'EOF'
|
||||||
cat > "app/routes.py" << 'EOF'
|
|
||||||
from flask import Blueprint, render_template
|
from flask import Blueprint, render_template
|
||||||
|
|
||||||
main = Blueprint('main', __name__)
|
main = Blueprint('main', __name__)
|
||||||
|
|
||||||
@main.route('/')
|
@main.route('/')
|
||||||
def index():
|
def index():
|
||||||
return render_template('index.html')
|
return render_template('index.html')
|
||||||
EOF
|
EOF
|
||||||
|
echo '<!DOCTYPE html><html><body><h1>Flask</h1></body></html>' > "$1/app/templates/index.html"
|
||||||
cat > "app.py" << 'EOF'
|
cat > "$1/app.py" << 'EOF'
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
from app import create_app
|
from app import create_app
|
||||||
|
|
||||||
app = create_app()
|
app = create_app()
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app.run(debug=True, host='0.0.0.0', port=5000)
|
app.run(debug=True)
|
||||||
EOF
|
EOF
|
||||||
chmod +x app.py
|
echo "Flask>=3.0.0" > "$1/requirements.txt"
|
||||||
|
_py_gitignore "$1"; _py_git "$1"
|
||||||
cat > "app/templates/index.html" << 'EOF'
|
df_print_success "Created: $1"
|
||||||
<!DOCTYPE html>
|
_py_next "$1"
|
||||||
<html><head><title>Flask App</title></head>
|
|
||||||
<body><h1>Welcome to Flask!</h1></body></html>
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat > "requirements.txt" << 'EOF'
|
|
||||||
Flask>=3.0.0
|
|
||||||
python-decouple>=3.8
|
|
||||||
EOF
|
|
||||||
|
|
||||||
_py_create_gitignore "."
|
|
||||||
_py_init_git "."
|
|
||||||
cd ..
|
|
||||||
|
|
||||||
echo
|
|
||||||
_py_print_success "Flask project '$project_name' created!"
|
|
||||||
_py_show_next_steps "$project_name" "true"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# FastAPI Project Template
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
py-fastapi() {
|
py-fastapi() {
|
||||||
local project_name="$1"
|
_py_check_name "$1" || return 1
|
||||||
_py_check_project_name "$project_name" || return 1
|
df_print_func_name "FastAPI Project: $1"
|
||||||
|
mkdir -p "$1"/{app,tests}
|
||||||
df_print_func_name "FastAPI Project: $project_name"
|
_py_venv "$1"
|
||||||
|
df_print_step "Installing FastAPI"
|
||||||
mkdir -p "$project_name"/{app/{api,models,schemas},tests}
|
"$1/$PY_VENV/bin/pip" install fastapi uvicorn -q
|
||||||
_py_create_venv "$project_name"
|
cat > "$1/app/main.py" << 'EOF'
|
||||||
|
|
||||||
cd "$project_name"
|
|
||||||
_py_print_step "Installing FastAPI"
|
|
||||||
"$PY_TEMPLATE_VENV_NAME/bin/pip" install fastapi uvicorn[standard] pydantic
|
|
||||||
|
|
||||||
touch "app/__init__.py"
|
|
||||||
|
|
||||||
cat > "app/main.py" << 'EOF'
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
app = FastAPI()
|
||||||
|
|
||||||
app = FastAPI(title="My API", version="0.1.0")
|
|
||||||
|
|
||||||
app.add_middleware(
|
|
||||||
CORSMiddleware,
|
|
||||||
allow_origins=["*"],
|
|
||||||
allow_methods=["*"],
|
|
||||||
allow_headers=["*"],
|
|
||||||
)
|
|
||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
def root():
|
def root():
|
||||||
return {"message": "Welcome to FastAPI"}
|
return {"message": "Hello"}
|
||||||
|
|
||||||
@app.get("/health")
|
@app.get("/health")
|
||||||
def health():
|
def health():
|
||||||
return {"status": "healthy"}
|
return {"status": "ok"}
|
||||||
EOF
|
EOF
|
||||||
|
cat > "$1/run.py" << 'EOF'
|
||||||
cat > "run.py" << 'EOF'
|
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
uvicorn.run("app.main:app", host="0.0.0.0", port=8000, reload=True)
|
uvicorn.run("app.main:app", host="0.0.0.0", port=8000, reload=True)
|
||||||
EOF
|
EOF
|
||||||
chmod +x run.py
|
echo -e "fastapi>=0.104.0\nuvicorn>=0.24.0" > "$1/requirements.txt"
|
||||||
|
_py_gitignore "$1"; _py_git "$1"
|
||||||
cat > "requirements.txt" << 'EOF'
|
df_print_success "Created: $1"
|
||||||
fastapi>=0.104.0
|
df_print_info "Docs: http://localhost:8000/docs"
|
||||||
uvicorn[standard]>=0.24.0
|
_py_next "$1"
|
||||||
pydantic>=2.5.0
|
|
||||||
EOF
|
|
||||||
|
|
||||||
_py_create_gitignore "."
|
|
||||||
_py_init_git "."
|
|
||||||
cd ..
|
|
||||||
|
|
||||||
echo
|
|
||||||
_py_print_success "FastAPI project '$project_name' created!"
|
|
||||||
_py_print_info "Docs at: http://localhost:8000/docs"
|
|
||||||
_py_show_next_steps "$project_name" "true"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Data Science Project Template
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
py-data() {
|
|
||||||
local project_name="$1"
|
|
||||||
_py_check_project_name "$project_name" || return 1
|
|
||||||
|
|
||||||
df_print_func_name "Data Science Project: $project_name"
|
|
||||||
|
|
||||||
mkdir -p "$project_name"/{data/{raw,processed},notebooks,src,models,reports/figures}
|
|
||||||
_py_create_venv "$project_name"
|
|
||||||
|
|
||||||
cd "$project_name"
|
|
||||||
_py_print_step "Installing data science packages"
|
|
||||||
"$PY_TEMPLATE_VENV_NAME/bin/pip" install pandas numpy matplotlib seaborn jupyter
|
|
||||||
|
|
||||||
touch "src/__init__.py"
|
|
||||||
touch data/raw/.gitkeep data/processed/.gitkeep
|
|
||||||
|
|
||||||
cat > "requirements.txt" << 'EOF'
|
|
||||||
pandas>=2.1.0
|
|
||||||
numpy>=1.24.0
|
|
||||||
matplotlib>=3.8.0
|
|
||||||
seaborn>=0.13.0
|
|
||||||
jupyter>=1.0.0
|
|
||||||
scikit-learn>=1.3.0
|
|
||||||
EOF
|
|
||||||
|
|
||||||
_py_create_gitignore "."
|
|
||||||
cat >> ".gitignore" << 'EOF'
|
|
||||||
*.pkl
|
|
||||||
*.h5
|
|
||||||
*.parquet
|
|
||||||
data/raw/*
|
|
||||||
data/processed/*
|
|
||||||
!data/raw/.gitkeep
|
|
||||||
!data/processed/.gitkeep
|
|
||||||
models/*.pkl
|
|
||||||
.ipynb_checkpoints
|
|
||||||
EOF
|
|
||||||
|
|
||||||
_py_init_git "."
|
|
||||||
cd ..
|
|
||||||
|
|
||||||
echo
|
|
||||||
_py_print_success "Data science project '$project_name' created!"
|
|
||||||
_py_print_info "Start Jupyter: jupyter notebook"
|
|
||||||
_py_show_next_steps "$project_name" "true"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# CLI Tool Project Template
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
py-cli() {
|
py-cli() {
|
||||||
local project_name="$1"
|
_py_check_name "$1" || return 1
|
||||||
_py_check_project_name "$project_name" || return 1
|
df_print_func_name "CLI Project: $1"
|
||||||
|
mkdir -p "$1"/{src/$1,tests}
|
||||||
df_print_func_name "CLI Tool Project: $project_name"
|
_py_venv "$1"
|
||||||
|
df_print_step "Installing click"
|
||||||
mkdir -p "$project_name"/{src/$project_name,tests}
|
"$1/$PY_VENV/bin/pip" install click -q
|
||||||
_py_create_venv "$project_name"
|
echo '__version__ = "0.1.0"' > "$1/src/$1/__init__.py"
|
||||||
|
cat > "$1/src/$1/cli.py" << 'EOF'
|
||||||
cd "$project_name"
|
|
||||||
_py_print_step "Installing click"
|
|
||||||
"$PY_TEMPLATE_VENV_NAME/bin/pip" install click
|
|
||||||
|
|
||||||
cat > "src/$project_name/__init__.py" << 'EOF'
|
|
||||||
__version__ = "0.1.0"
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat > "src/$project_name/cli.py" << 'EOF'
|
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import click
|
import click
|
||||||
|
|
||||||
@click.group()
|
@click.group()
|
||||||
@click.version_option()
|
@click.version_option()
|
||||||
def cli():
|
def cli():
|
||||||
"""CLI tool - A command-line utility."""
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
@click.argument('name', default='World')
|
@click.argument('name', default='World')
|
||||||
def greet(name):
|
def greet(name):
|
||||||
"""Greet someone."""
|
|
||||||
click.echo(f"Hello, {name}!")
|
click.echo(f"Hello, {name}!")
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
cli()
|
cli()
|
||||||
EOF
|
EOF
|
||||||
|
echo "click>=8.1.0" > "$1/requirements.txt"
|
||||||
cat > "setup.py" << EOF
|
_py_gitignore "$1"; _py_git "$1"
|
||||||
from setuptools import setup, find_packages
|
df_print_success "Created: $1"
|
||||||
|
df_print_info "Install: pip install -e $1"
|
||||||
setup(
|
_py_next "$1"
|
||||||
name="$project_name",
|
|
||||||
version="0.1.0",
|
|
||||||
packages=find_packages(where="src"),
|
|
||||||
package_dir={"": "src"},
|
|
||||||
install_requires=["click>=8.0.0"],
|
|
||||||
entry_points={"console_scripts": ["$project_name=$project_name.cli:cli"]},
|
|
||||||
)
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat > "requirements.txt" << 'EOF'
|
|
||||||
click>=8.1.0
|
|
||||||
EOF
|
|
||||||
|
|
||||||
_py_create_gitignore "."
|
|
||||||
_py_init_git "."
|
|
||||||
cd ..
|
|
||||||
|
|
||||||
echo
|
|
||||||
_py_print_success "CLI tool project '$project_name' created!"
|
|
||||||
_py_print_info "Install with: pip install -e $project_name"
|
|
||||||
_py_show_next_steps "$project_name" "true"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Aliases
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
alias pynew='py-new'
|
|
||||||
alias pydjango='py-django'
|
|
||||||
alias pyflask='py-flask'
|
|
||||||
alias pyfast='py-fastapi'
|
|
||||||
alias pydata='py-data'
|
|
||||||
alias pycli='py-cli'
|
|
||||||
|
|
||||||
# Quick venv activation
|
|
||||||
venv() {
|
venv() {
|
||||||
if [[ -d "venv" ]]; then
|
[[ -d "venv" ]] && source venv/bin/activate && return
|
||||||
source venv/bin/activate
|
[[ -d ".venv" ]] && source .venv/bin/activate && return
|
||||||
elif [[ -d ".venv" ]]; then
|
df_print_error "No venv found"
|
||||||
source .venv/bin/activate
|
|
||||||
elif [[ -d "env" ]]; then
|
|
||||||
source env/bin/activate
|
|
||||||
else
|
|
||||||
echo "No virtual environment found (venv, .venv, or env)"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
alias pynew='py-new' pyflask='py-flask' pyfast='py-fastapi' pycli='py-cli'
|
||||||
|
|||||||
@@ -1,291 +1,82 @@
|
|||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Smart Command Suggestions for Zsh
|
# Smart Command Suggestions for Zsh
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Provides intelligent suggestions when commands fail or could be improved
|
|
||||||
#
|
|
||||||
# Features:
|
|
||||||
# - Typo correction for common commands
|
|
||||||
# - Suggests existing aliases for frequently typed commands
|
|
||||||
# - "Did you mean?" for unknown commands
|
|
||||||
# - Package installation suggestions for missing commands
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
# Source shared colors (with fallback)
|
source "${0:A:h}/../lib/utils.zsh" 2>/dev/null || \
|
||||||
source "${0:A:h}/../lib/colors.zsh" 2>/dev/null || \
|
source "$HOME/.dotfiles/zsh/lib/utils.zsh" 2>/dev/null
|
||||||
source "$HOME/.dotfiles/zsh/lib/colors.zsh" 2>/dev/null || {
|
|
||||||
typeset -g DF_CYAN=$'\033[0;36m' DF_YELLOW=$'\033[1;33m'
|
|
||||||
typeset -g DF_GREEN=$'\033[0;32m' DF_RED=$'\033[0;31m'
|
|
||||||
typeset -g DF_DIM=$'\033[2m' DF_NC=$'\033[0m'
|
|
||||||
}
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Configuration
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
typeset -g SMART_SUGGEST_ENABLED=true
|
typeset -g SMART_SUGGEST_ENABLED=true
|
||||||
typeset -g SMART_SUGGEST_TYPOS=true
|
|
||||||
typeset -g SMART_SUGGEST_ALIASES=true
|
|
||||||
typeset -g SMART_SUGGEST_PACKAGES=true
|
|
||||||
typeset -g SMART_SUGGEST_HISTORY=true
|
|
||||||
typeset -g SMART_SUGGEST_TRACK_FILE="$HOME/.cache/smart-suggest-track"
|
typeset -g SMART_SUGGEST_TRACK_FILE="$HOME/.cache/smart-suggest-track"
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Common Typo Database
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
typeset -gA TYPO_CORRECTIONS=(
|
typeset -gA TYPO_CORRECTIONS=(
|
||||||
# Git typos
|
[gti]="git" [gitt]="git" [got]="git" [gi]="git"
|
||||||
[gti]="git" [gitt]="git" [got]="git" [gut]="git" [gi]="git"
|
[gitst]="git st" [gits]="git s" [gitp]="git p"
|
||||||
[giit]="git" [ggit]="git" [gitst]="git st" [gits]="git s"
|
[psuh]="push" [psull]="pull" [pul]="pull"
|
||||||
[gitl]="git l" [gitd]="git d" [gitp]="git p"
|
[stauts]="status" [comit]="commit" [commti]="commit"
|
||||||
[psuh]="push" [psull]="pull" [pul]="pull" [puhs]="push"
|
[chekcout]="checkout" [branhc]="branch" [marge]="merge"
|
||||||
[stauts]="status" [statis]="status" [statuus]="status"
|
[dokcer]="docker" [doker]="docker" [dcoker]="docker"
|
||||||
[comit]="commit" [commti]="commit" [commt]="commit"
|
[sl]="ls" [sls]="ls" [cta]="cat" [grpe]="grep" [gerp]="grep"
|
||||||
[chekcout]="checkout" [chekout]="checkout" [checkou]="checkout"
|
[mkdri]="mkdir" [chmdo]="chmod" [suod]="sudo" [sduo]="sudo"
|
||||||
[branhc]="branch" [barnch]="branch" [bracnh]="branch"
|
[pytohn]="python" [pyhton]="python" [ndoe]="node"
|
||||||
[marge]="merge" [merg]="merge" [stsh]="stash" [stahs]="stash"
|
[vmi]="vim" [cde]="code" [clera]="clear" [exti]="exit"
|
||||||
|
|
||||||
# Docker typos
|
|
||||||
[dokcer]="docker" [doker]="docker" [docekr]="docker"
|
|
||||||
[dcoker]="docker" [dockr]="docker" [docke]="docker"
|
|
||||||
[docker-compoes]="docker-compose" [docker-compsoe]="docker-compose"
|
|
||||||
[dokcer-compose]="docker-compose"
|
|
||||||
|
|
||||||
# Common command typos
|
|
||||||
[sl]="ls" [l]="ls" [sls]="ls" [lss]="ls"
|
|
||||||
[cta]="cat" [catt]="cat" [caat]="cat"
|
|
||||||
[grpe]="grep" [gerp]="grep" [gre]="grep" [grepp]="grep"
|
|
||||||
[mkdri]="mkdir" [mkdr]="mkdir" [mdkir]="mkdir" [mdir]="mkdir"
|
|
||||||
[rn]="rm" [rmm]="rm" [chmdo]="chmod" [chomd]="chmod"
|
|
||||||
[chonw]="chown" [cown]="chown" [tarr]="tar" [tart]="tar"
|
|
||||||
[wegt]="wget" [wgte]="wget" [weget]="wget"
|
|
||||||
[crul]="curl" [curll]="curl"
|
|
||||||
[pytohn]="python" [pyhton]="python" [pythn]="python"
|
|
||||||
[pyton]="python" [pthon]="python" [pytho]="python"
|
|
||||||
[ndoe]="node" [noed]="node" [noode]="node"
|
|
||||||
[npn]="npm" [nmpm]="npm" [nppm]="npm"
|
|
||||||
[yran]="yarn" [yaarn]="yarn" [yanr]="yarn"
|
|
||||||
[suod]="sudo" [sudi]="sudo" [sduo]="sudo" [sudoo]="sudo"
|
|
||||||
[sssh]="ssh" [shh]="ssh" [scpp]="scp" [spcp]="scp"
|
|
||||||
[vmi]="vim" [imv]="vim" [viim]="vim"
|
|
||||||
[cde]="code" [cdoe]="code" [cod]="code"
|
|
||||||
[clera]="clear" [cler]="clear" [claer]="clear"
|
|
||||||
[ecoh]="echo" [ehco]="echo" [echoo]="echo"
|
|
||||||
[exti]="exit" [ext]="exit" [exitt]="exit" [eixt]="exit"
|
|
||||||
[histroy]="history" [hisotry]="history" [hsitory]="history"
|
|
||||||
[histrory]="history" [maek]="make" [mkae]="make"
|
|
||||||
[amke]="make" [makee]="make" [ccd]="cd" [cdd]="cd"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Package Manager Detection
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
_ss_get_package_manager() {
|
|
||||||
if command -v apt-get &>/dev/null; then echo "apt"
|
|
||||||
elif command -v dnf &>/dev/null; then echo "dnf"
|
|
||||||
elif command -v pacman &>/dev/null; then echo "pacman"
|
|
||||||
elif command -v brew &>/dev/null; then echo "brew"
|
|
||||||
else echo ""
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
typeset -gA COMMAND_PACKAGES=(
|
typeset -gA COMMAND_PACKAGES=(
|
||||||
[htop]="htop" [tree]="tree" [jq]="jq"
|
[htop]="htop" [tree]="tree" [jq]="jq" [fd]="fd" [rg]="ripgrep"
|
||||||
[fd]="fd-find:apt fd:pacman fd:brew" [rg]="ripgrep"
|
[bat]="bat" [eza]="eza" [fzf]="fzf" [tldr]="tldr" [ncdu]="ncdu"
|
||||||
[bat]="bat" [eza]="eza" [exa]="exa" [fzf]="fzf"
|
[lazygit]="lazygit" [neofetch]="neofetch" [delta]="git-delta"
|
||||||
[tldr]="tldr" [ncdu]="ncdu" [duf]="duf" [dust]="dust"
|
|
||||||
[procs]="procs" [bottom]="bottom" [btm]="bottom"
|
|
||||||
[lazygit]="lazygit" [lazydocker]="lazydocker"
|
|
||||||
[neofetch]="neofetch" [fastfetch]="fastfetch"
|
|
||||||
[httpie]="httpie" [http]="httpie"
|
|
||||||
[delta]="git-delta:apt delta:pacman git-delta:brew"
|
|
||||||
[glow]="glow" [navi]="navi"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
_ss_suggest_package() {
|
_ss_track() {
|
||||||
local cmd="$1"
|
|
||||||
local pm=$(_ss_get_package_manager)
|
|
||||||
|
|
||||||
[[ -z "$pm" ]] && return 1
|
|
||||||
|
|
||||||
local pkg_info="${COMMAND_PACKAGES[$cmd]}"
|
|
||||||
[[ -z "$pkg_info" ]] && return 1
|
|
||||||
|
|
||||||
local pkg=""
|
|
||||||
|
|
||||||
if [[ "$pkg_info" == *":"* ]]; then
|
|
||||||
for entry in ${(s: :)pkg_info}; do
|
|
||||||
local p="${entry%%:*}"
|
|
||||||
local m="${entry##*:}"
|
|
||||||
if [[ "$m" == "$pm" ]]; then
|
|
||||||
pkg="$p"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
[[ -z "$pkg" ]] && pkg="${${(s: :)pkg_info}[1]%%:*}"
|
|
||||||
else
|
|
||||||
pkg="$pkg_info"
|
|
||||||
fi
|
|
||||||
|
|
||||||
[[ -z "$pkg" ]] && return 1
|
|
||||||
|
|
||||||
local install_cmd=""
|
|
||||||
case "$pm" in
|
|
||||||
apt) install_cmd="sudo apt install $pkg" ;;
|
|
||||||
dnf) install_cmd="sudo dnf install $pkg" ;;
|
|
||||||
pacman) install_cmd="sudo pacman -S $pkg" ;;
|
|
||||||
brew) install_cmd="brew install $pkg" ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
echo "$install_cmd"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Alias Tracking
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
_ss_track_command() {
|
|
||||||
[[ "$SMART_SUGGEST_ALIASES" != true ]] && return
|
|
||||||
|
|
||||||
local cmd="$1"
|
local cmd="$1"
|
||||||
[[ ${#cmd} -lt 8 ]] && return
|
[[ ${#cmd} -lt 8 ]] && return
|
||||||
|
df_ensure_dir "$(dirname "$SMART_SUGGEST_TRACK_FILE")"
|
||||||
mkdir -p "$(dirname "$SMART_SUGGEST_TRACK_FILE")"
|
|
||||||
echo "$cmd" >> "$SMART_SUGGEST_TRACK_FILE"
|
echo "$cmd" >> "$SMART_SUGGEST_TRACK_FILE"
|
||||||
|
|
||||||
local count=$(grep -Fc "$cmd" "$SMART_SUGGEST_TRACK_FILE" 2>/dev/null || echo 0)
|
local count=$(grep -Fc "$cmd" "$SMART_SUGGEST_TRACK_FILE" 2>/dev/null || echo 0)
|
||||||
|
if (( count >= 10 && count % 10 == 0 )); then
|
||||||
if [[ $count -ge 10 && $((count % 10)) -eq 0 ]]; then
|
local existing=$(alias | grep -F "='$cmd'" | head -1 | cut -d= -f1)
|
||||||
_ss_suggest_alias_for "$cmd" "$count"
|
[[ -n "$existing" ]] && df_print_info "You have alias: $existing" || \
|
||||||
|
df_print_info "Consider: alias xyz='$cmd'"
|
||||||
fi
|
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() {
|
command_not_found_handler() {
|
||||||
local cmd="$1"
|
local cmd="$1"; shift
|
||||||
shift
|
[[ "$SMART_SUGGEST_ENABLED" != true ]] && { echo "zsh: command not found: $cmd"; return 127; }
|
||||||
local args="$@"
|
|
||||||
|
|
||||||
[[ "$SMART_SUGGEST_ENABLED" != true ]] && {
|
df_print_error "Command not found: $cmd"
|
||||||
echo "zsh: command not found: $cmd"
|
|
||||||
return 127
|
|
||||||
}
|
|
||||||
|
|
||||||
echo -e "${DF_RED}✗${DF_NC} Command not found: ${DF_YELLOW}$cmd${DF_NC}"
|
local correction="${TYPO_CORRECTIONS[$cmd]}"
|
||||||
|
[[ -n "$correction" ]] && { df_print_info "Did you mean: $correction?"; df_print_indent "Run: $correction $@"; }
|
||||||
|
|
||||||
local suggestion_made=false
|
local pkg="${COMMAND_PACKAGES[$cmd]}"
|
||||||
|
[[ -n "$pkg" ]] && df_print_info "Install: sudo pacman -S $pkg"
|
||||||
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
|
|
||||||
|
|
||||||
return 127
|
return 127
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Hooks
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
_ss_preexec_hook() {
|
|
||||||
local cmd="$1"
|
|
||||||
local first_word="${cmd%% *}"
|
|
||||||
_ss_track_command "$cmd"
|
|
||||||
}
|
|
||||||
|
|
||||||
_ss_precmd_hook() {
|
|
||||||
local exit_code=$?
|
|
||||||
[[ $exit_code -eq 0 ]] && return
|
|
||||||
|
|
||||||
local last_cmd=$(fc -ln -1 2>/dev/null | sed 's/^[[:space:]]*//')
|
|
||||||
[[ -z "$last_cmd" ]] && return
|
|
||||||
|
|
||||||
local first_word="${last_cmd%% *}"
|
|
||||||
|
|
||||||
if [[ "$first_word" == "git" && $exit_code -ne 0 ]]; then
|
|
||||||
local git_subcmd=$(echo "$last_cmd" | awk '{print $2}')
|
|
||||||
local correction="${TYPO_CORRECTIONS[$git_subcmd]}"
|
|
||||||
|
|
||||||
if [[ -n "$correction" ]]; then
|
|
||||||
echo -e "${DF_CYAN}→${DF_NC} Did you mean: ${DF_GREEN}git $correction${DF_NC}?"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Quick Fix Function
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
fuck() {
|
fuck() {
|
||||||
local last_cmd=$(fc -ln -1 2>/dev/null | sed 's/^[[:space:]]*//')
|
local last=$(fc -ln -1 2>/dev/null | sed 's/^[[:space:]]*//')
|
||||||
local first_word="${last_cmd%% *}"
|
local first="${last%% *}"
|
||||||
|
local fix="${TYPO_CORRECTIONS[$first]}"
|
||||||
local correction="${TYPO_CORRECTIONS[$first_word]}"
|
[[ -n "$fix" ]] && { df_print_step "Running: ${last/$first/$fix}"; eval "${last/$first/$fix}"; } || df_print_warning "No fix for: $last"
|
||||||
|
|
||||||
if [[ -n "$correction" ]]; then
|
|
||||||
local fixed_cmd="${last_cmd/$first_word/$correction}"
|
|
||||||
echo -e "${DF_GREEN}Running:${DF_NC} $fixed_cmd"
|
|
||||||
eval "$fixed_cmd"
|
|
||||||
else
|
|
||||||
echo "No automatic fix available"
|
|
||||||
echo "Last command: $last_cmd"
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================================================
|
_ss_preexec() { _ss_track "$1"; }
|
||||||
# Setup Hooks
|
_ss_precmd() {
|
||||||
# ============================================================================
|
local exit=$?; (( exit == 0 )) && return
|
||||||
|
local last=$(fc -ln -1 2>/dev/null | sed 's/^[[:space:]]*//')
|
||||||
|
[[ "${last%% *}" == "git" ]] && {
|
||||||
|
local sub=$(echo "$last" | awk '{print $2}')
|
||||||
|
local fix="${TYPO_CORRECTIONS[$sub]}"
|
||||||
|
[[ -n "$fix" ]] && df_print_info "Did you mean: git $fix?"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_ss_setup() {
|
_ss_setup() {
|
||||||
autoload -Uz add-zsh-hook
|
autoload -Uz add-zsh-hook
|
||||||
add-zsh-hook preexec _ss_preexec_hook
|
add-zsh-hook preexec _ss_preexec
|
||||||
add-zsh-hook precmd _ss_precmd_hook
|
add-zsh-hook precmd _ss_precmd
|
||||||
}
|
}
|
||||||
|
|
||||||
[[ "$SMART_SUGGEST_ENABLED" == true ]] && _ss_setup
|
[[ "$SMART_SUGGEST_ENABLED" == true ]] && _ss_setup
|
||||||
|
|||||||
@@ -2,253 +2,97 @@
|
|||||||
# Snapper Snapshot Functions for CachyOS/Arch with limine-snapper-sync
|
# Snapper Snapshot Functions for CachyOS/Arch with limine-snapper-sync
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
# Source shared colors (with fallback)
|
source "${0:A:h}/../lib/utils.zsh" 2>/dev/null || \
|
||||||
source "${0:A:h}/../lib/colors.zsh" 2>/dev/null || \
|
source "$HOME/.dotfiles/zsh/lib/utils.zsh" 2>/dev/null
|
||||||
source "$HOME/.dotfiles/zsh/lib/colors.zsh" 2>/dev/null || {
|
|
||||||
typeset -g DF_GREEN=$'\033[0;32m' DF_YELLOW=$'\033[1;33m'
|
|
||||||
typeset -g DF_RED=$'\033[0;31m' DF_BLUE=$'\033[0;34m' DF_NC=$'\033[0m'
|
|
||||||
}
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Main Snapshot Function with Limine Validation
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
snap-create() {
|
snap-create() {
|
||||||
local description="$*"
|
local desc="$*"
|
||||||
local snap_config="root"
|
local limine="/boot/limine.conf"
|
||||||
local limine_conf="/boot/limine.conf"
|
|
||||||
|
|
||||||
df_print_func_name "Snapper Snapshot Creation"
|
df_print_func_name "Snapper Snapshot Creation"
|
||||||
|
|
||||||
if [[ -z "$description" ]]; then
|
if [[ -z "$desc" ]]; then
|
||||||
echo -e "${DF_YELLOW}⚠${DF_NC} No description provided"
|
df_print_warning "No description"
|
||||||
echo -n "Enter snapshot description: "
|
echo -n "Description: "; read desc
|
||||||
read description
|
[[ -z "$desc" ]] && { df_print_error "Required"; return 1; }
|
||||||
[[ -z "$description" ]] && { echo -e "${DF_RED}✗${DF_NC} Description required. Aborting."; return 1; }
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
[[ ! -f "$limine_conf" ]] && { echo -e "${DF_RED}✗${DF_NC} Limine config not found: $limine_conf"; return 1; }
|
[[ ! -f "$limine" ]] && { df_print_error "Limine not found: $limine"; return 1; }
|
||||||
|
|
||||||
echo -e "${DF_BLUE}==>${DF_NC} Checking limine.conf state before snapshot"
|
df_print_step "Checking limine.conf before snapshot"
|
||||||
local before_checksum=$(sudo md5sum "$limine_conf" | awk '{print $1}')
|
local before=$(sudo grep -cP "^\\s*///\\d+\\s*│" "$limine" || echo "0")
|
||||||
local before_entries=$(sudo grep -cP "^\\s*///\\d+\\s*│" "$limine_conf" || echo "0")
|
df_print_success "Before: $before entries"
|
||||||
|
|
||||||
echo -e "${DF_GREEN}✓${DF_NC} Before: $before_entries snapshot entries"
|
df_print_step "Creating snapshot: \"$desc\""
|
||||||
echo -e "${DF_GREEN}✓${DF_NC} Before checksum: $before_checksum"
|
local num=$(sudo snapper -c root create --description "$desc" --print-number)
|
||||||
|
[[ -z "$num" ]] && { df_print_error "Failed"; return 1; }
|
||||||
|
df_print_success "Created: #$num"
|
||||||
|
|
||||||
echo -e "\n${DF_BLUE}==>${DF_NC} Creating snapshot: \"$description\""
|
df_print_step "Triggering limine-snapper-sync..."
|
||||||
|
sudo systemctl start limine-snapper-sync.service && df_print_success "Triggered" || df_print_warning "May run automatically"
|
||||||
local snapshot_num=$(sudo snapper -c "$snap_config" create --description "$description" --print-number)
|
|
||||||
|
|
||||||
[[ -z "$snapshot_num" ]] && { echo -e "${DF_RED}✗${DF_NC} Failed to create snapshot"; return 1; }
|
|
||||||
|
|
||||||
echo -e "${DF_GREEN}✓${DF_NC} Snapshot created: #$snapshot_num"
|
|
||||||
|
|
||||||
echo -e "\n${DF_BLUE}==>${DF_NC} Triggering limine-snapper-sync service..."
|
|
||||||
|
|
||||||
if sudo systemctl start limine-snapper-sync.service; then
|
|
||||||
echo -e "${DF_GREEN}✓${DF_NC} Service triggered successfully"
|
|
||||||
else
|
|
||||||
echo -e "${DF_YELLOW}⚠${DF_NC} Failed to trigger service (may run automatically)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo -e "${DF_BLUE}==>${DF_NC} Waiting for limine-snapper-sync to update limine.conf..."
|
|
||||||
sleep 2
|
sleep 2
|
||||||
|
|
||||||
echo -e "${DF_BLUE}==>${DF_NC} Validating limine.conf update"
|
df_print_step "Validating"
|
||||||
local after_checksum=$(sudo md5sum "$limine_conf" | awk '{print $1}')
|
local after=$(sudo grep -cP "^\\s*///\\d+\\s*│" "$limine" || echo "0")
|
||||||
local after_entries=$(sudo grep -cP "^\\s*///\\d+\\s*│" "$limine_conf" || echo "0")
|
|
||||||
|
|
||||||
local validation_passed=true
|
if sudo grep -qP "^\\s*///$num\\s*│" "$limine"; then
|
||||||
|
df_print_success "Snapshot #$num in limine.conf"
|
||||||
if [[ "$before_checksum" == "$after_checksum" ]]; then
|
(( after > before )) && df_print_success "Added $((after - before)) entry"
|
||||||
echo -e "${DF_RED}✗${DF_NC} limine.conf was NOT updated (checksum unchanged)"
|
|
||||||
validation_passed=false
|
|
||||||
else
|
else
|
||||||
echo -e "${DF_GREEN}✓${DF_NC} limine.conf was updated"
|
df_print_error "Snapshot #$num NOT in limine.conf"
|
||||||
fi
|
return 1
|
||||||
|
|
||||||
if [[ "$after_entries" -le "$before_entries" ]]; then
|
|
||||||
echo -e "${DF_RED}✗${DF_NC} No new snapshot entry added to limine.conf"
|
|
||||||
validation_passed=false
|
|
||||||
else
|
|
||||||
local new_entries=$((after_entries - before_entries))
|
|
||||||
echo -e "${DF_GREEN}✓${DF_NC} Added $new_entries new snapshot entry/entries"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo -e "\n${DF_BLUE}==>${DF_NC} Searching for snapshot #$snapshot_num in limine.conf"
|
|
||||||
|
|
||||||
if sudo grep -qP "^\\s*///$snapshot_num\\s*│" "$limine_conf"; then
|
|
||||||
echo -e "${DF_GREEN}✓${DF_NC} Found snapshot #$snapshot_num in limine.conf"
|
|
||||||
echo -e "\n${DF_BLUE}Snapshot entry:${DF_NC}"
|
|
||||||
local entry_line=$(sudo grep -nP "^\\s*///$snapshot_num\\s*│" "$limine_conf" | head -n 1 | cut -d: -f1)
|
|
||||||
[[ -n "$entry_line" ]] && sudo sed -n "${entry_line}p; $((entry_line+1))p" "$limine_conf" | sed 's/^/ /'
|
|
||||||
else
|
|
||||||
echo -e "${DF_RED}✗${DF_NC} Snapshot #$snapshot_num NOT found in limine.conf"
|
|
||||||
validation_passed=false
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${DF_CYAN}Summary:${DF_NC}"
|
df_print_section "Summary"
|
||||||
echo -e " Snapshot Number: #$snapshot_num"
|
df_print_indent "Number: #$num"
|
||||||
echo -e " Description: \"$description\""
|
df_print_indent "Description: $desc"
|
||||||
echo -e " Config: $snap_config"
|
|
||||||
echo -e " Before entries: $before_entries"
|
|
||||||
echo -e " After entries: $after_entries"
|
|
||||||
|
|
||||||
if [[ "$validation_passed" == true ]]; then
|
|
||||||
echo -e " Status: ${DF_GREEN}✓ VALIDATED${DF_NC}"
|
|
||||||
echo -e "\n${DF_GREEN}✓${DF_NC} Snapshot created and limine.conf successfully updated!"
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
echo -e " Status: ${DF_RED}✗ VALIDATION FAILED${DF_NC}"
|
|
||||||
echo -e "\n${DF_RED}✗${DF_NC} Snapshot created but limine.conf validation failed!"
|
|
||||||
echo -e "${DF_YELLOW}⚠${DF_NC} Check if limine-snapper-sync service is running properly"
|
|
||||||
echo -e "${DF_YELLOW}Run:${DF_NC} sudo systemctl status limine-snapper-sync.service"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Helper Functions
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
snap-list() {
|
snap-list() {
|
||||||
local count="${1:-10}"
|
local count="${1:-10}"
|
||||||
|
|
||||||
df_print_func_name "Snapper Snapshots (last $count)"
|
df_print_func_name "Snapper Snapshots (last $count)"
|
||||||
|
|
||||||
sudo snapper -c root list | tail -n "$((count + 1))"
|
sudo snapper -c root list | tail -n "$((count + 1))"
|
||||||
}
|
}
|
||||||
|
|
||||||
snap-show() {
|
snap-show() {
|
||||||
[[ -z "$1" ]] && { echo -e "${DF_RED}✗${DF_NC} Usage: snap-show <snapshot_number>"; return 1; }
|
[[ -z "$1" ]] && { echo "Usage: snap-show <num>"; return 1; }
|
||||||
|
df_print_func_name "Snapshot #$1"
|
||||||
df_print_func_name "Snapshot #$1 Details"
|
|
||||||
|
|
||||||
sudo snapper -c root list | grep "^\s*$1\s"
|
sudo snapper -c root list | grep "^\s*$1\s"
|
||||||
|
echo ""
|
||||||
echo -e "\n${DF_CYAN}In limine.conf:${DF_NC}"
|
df_print_section "In limine.conf"
|
||||||
if sudo grep -qP "^\\s*///$1\\s*│" /boot/limine.conf; then
|
sudo grep -qP "^\\s*///$1\\s*│" /boot/limine.conf && \
|
||||||
local entry_line=$(sudo grep -nP "^\\s*///$1\\s*│" /boot/limine.conf | head -n 1 | cut -d: -f1)
|
sudo grep -P "^\\s*///$1\\s*│" /boot/limine.conf || df_print_warning "Not found"
|
||||||
[[ -n "$entry_line" ]] && sudo sed -n "${entry_line}p; $((entry_line+1))p" /boot/limine.conf
|
|
||||||
else
|
|
||||||
echo -e "${DF_YELLOW}⚠${DF_NC} Not found in limine.conf"
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
snap-delete() {
|
snap-delete() {
|
||||||
[[ -z "$1" ]] && { echo -e "${DF_RED}✗${DF_NC} Usage: snap-delete <snapshot_number>"; return 1; }
|
[[ -z "$1" ]] && { echo "Usage: snap-delete <num>"; return 1; }
|
||||||
|
df_print_func_name "Delete Snapshot #$1"
|
||||||
|
|
||||||
local snapshot_num="$1"
|
local before=$(sudo grep -cP "^\\s*///\\d+\\s*│" /boot/limine.conf || echo "0")
|
||||||
local limine_conf="/boot/limine.conf"
|
sudo snapper -c root delete "$1" && df_print_success "Deleted #$1" || { df_print_error "Failed"; return 1; }
|
||||||
|
|
||||||
df_print_func_name "Delete Snapshot #$snapshot_num"
|
df_print_step "Syncing limine..."
|
||||||
|
sudo systemctl start limine-snapper-sync.service; sleep 2
|
||||||
|
|
||||||
local before_entries=$(sudo grep -cP "^\\s*///\\d+\\s*│" "$limine_conf" || echo "0")
|
sudo grep -qP "^\\s*///$1\\s*│" /boot/limine.conf && df_print_error "Still in limine!" || df_print_success "Removed from limine"
|
||||||
|
|
||||||
sudo snapper -c root delete "$snapshot_num"
|
|
||||||
|
|
||||||
if [[ $? -eq 0 ]]; then
|
|
||||||
echo -e "${DF_GREEN}✓${DF_NC} Snapshot #$snapshot_num deleted"
|
|
||||||
|
|
||||||
echo -e "${DF_BLUE}==>${DF_NC} Triggering limine-snapper-sync..."
|
|
||||||
sudo systemctl start limine-snapper-sync.service
|
|
||||||
sleep 2
|
|
||||||
|
|
||||||
local after_entries=$(sudo grep -cP "^\\s*///\\d+\\s*│" "$limine_conf" || echo "0")
|
|
||||||
|
|
||||||
if [[ "$after_entries" -lt "$before_entries" ]]; then
|
|
||||||
echo -e "${DF_GREEN}✓${DF_NC} limine.conf updated (removed entry)"
|
|
||||||
else
|
|
||||||
echo -e "${DF_YELLOW}⚠${DF_NC} limine.conf may not have been updated"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! sudo grep -qP "^\\s*///$snapshot_num\\s*│" "$limine_conf"; then
|
|
||||||
echo -e "${DF_GREEN}✓${DF_NC} Snapshot #$snapshot_num removed from limine.conf"
|
|
||||||
else
|
|
||||||
echo -e "${DF_RED}✗${DF_NC} Snapshot #$snapshot_num still in limine.conf!"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo -e "${DF_RED}✗${DF_NC} Failed to delete snapshot #$snapshot_num"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
snap-check-limine() {
|
|
||||||
local limine_conf="/boot/limine.conf"
|
|
||||||
|
|
||||||
df_print_func_name "Limine Snapshot Entries"
|
|
||||||
|
|
||||||
[[ ! -f "$limine_conf" ]] && { echo -e "${DF_RED}✗${DF_NC} Limine config not found: $limine_conf"; return 1; }
|
|
||||||
|
|
||||||
local latest_snapshot=$(sudo snapper -c root list | tail -n +3 | grep -v "^\s*0\s" | tail -n 1 | awk '{print $1}')
|
|
||||||
[[ -z "$latest_snapshot" ]] && { echo -e "${DF_YELLOW}⚠${DF_NC} No snapshots found in snapper"; return 1; }
|
|
||||||
|
|
||||||
echo -e "${DF_CYAN}Latest snapshot:${DF_NC} #$latest_snapshot"
|
|
||||||
|
|
||||||
echo -e "${DF_BLUE}==>${DF_NC} Checking if latest snapshot is in limine.conf"
|
|
||||||
|
|
||||||
if sudo grep -qP "^\\s*///$latest_snapshot\s*│" "$limine_conf"; then
|
|
||||||
echo -e "${DF_GREEN}✓${DF_NC} Latest snapshot #$latest_snapshot is present in limine.conf"
|
|
||||||
else
|
|
||||||
echo -e "${DF_RED}✗${DF_NC} Latest snapshot #$latest_snapshot is NOT in limine.conf"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo -e "\n${DF_BLUE}==>${DF_NC} Counting snapshot entries"
|
|
||||||
local entry_count=$(sudo grep -cP "^\\s*///\\d+\\s*│" "$limine_conf" || echo "0")
|
|
||||||
echo -e "${DF_CYAN}Total snapshot entries:${DF_NC} $entry_count"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
snap-sync() {
|
snap-sync() {
|
||||||
df_print_func_name "Limine-Snapper-Sync"
|
df_print_func_name "Limine-Snapper-Sync"
|
||||||
|
df_print_step "Triggering sync..."
|
||||||
echo -e "${DF_BLUE}==>${DF_NC} Manually triggering limine-snapper-sync..."
|
sudo systemctl start limine-snapper-sync.service && { sleep 2; df_print_success "Done"; } || df_print_error "Failed"
|
||||||
|
|
||||||
if sudo systemctl start limine-snapper-sync.service; then
|
|
||||||
echo -e "${DF_GREEN}✓${DF_NC} Service triggered successfully"
|
|
||||||
sleep 2
|
|
||||||
echo -e "\n${DF_CYAN}Service status:${DF_NC}"
|
|
||||||
sudo systemctl status limine-snapper-sync.service --no-pager -l | tail -n 10
|
|
||||||
else
|
|
||||||
echo -e "${DF_RED}✗${DF_NC} Failed to trigger service"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
snap-validate-service() {
|
snap-check() {
|
||||||
df_print_func_name "Limine-Snapper-Sync Service Validation"
|
df_print_func_name "Limine Snapshot Entries"
|
||||||
|
local latest=$(sudo snapper -c root list | tail -n +3 | grep -v "^\s*0\s" | tail -1 | awk '{print $1}')
|
||||||
echo -e "${DF_BLUE}==>${DF_NC} Checking service unit"
|
[[ -z "$latest" ]] && { df_print_warning "No snapshots"; return 1; }
|
||||||
|
df_print_info "Latest: #$latest"
|
||||||
if systemctl list-unit-files | grep -q "limine-snapper-sync.service"; then
|
sudo grep -qP "^\\s*///$latest\\s*│" /boot/limine.conf && \
|
||||||
echo -e "${DF_GREEN}✓${DF_NC} limine-snapper-sync.service unit exists"
|
df_print_success "Latest in limine.conf" || df_print_error "Latest NOT in limine.conf"
|
||||||
else
|
local count=$(sudo grep -cP "^\\s*///\\d+\\s*│" /boot/limine.conf || echo "0")
|
||||||
echo -e "${DF_RED}✗${DF_NC} limine-snapper-sync.service unit NOT found"
|
df_print_info "Total entries: $count"
|
||||||
echo -e "\n${DF_YELLOW}Install with:${DF_NC} paru -S limine-snapper-sync"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo -e "\n${DF_BLUE}==>${DF_NC} Checking if service is enabled"
|
|
||||||
|
|
||||||
if systemctl is-enabled limine-snapper-sync.service &>/dev/null; then
|
|
||||||
echo -e "${DF_GREEN}✓${DF_NC} Service is enabled"
|
|
||||||
else
|
|
||||||
echo -e "${DF_YELLOW}⚠${DF_NC} Service is NOT enabled"
|
|
||||||
echo -e "${DF_YELLOW}Enable with:${DF_NC} sudo systemctl enable limine-snapper-sync.service"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo -e "\n${DF_BLUE}==>${DF_NC} Recent service logs (last 10 lines)"
|
|
||||||
echo ""
|
|
||||||
sudo journalctl -u limine-snapper-sync.service -n 10 --no-pager | sed 's/^/ /'
|
|
||||||
|
|
||||||
echo -e "\n${DF_GREEN}✓${DF_NC} Validation complete"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Quick snapshot aliases
|
alias snap='snap-create' snapls='snap-list' snaprm='snap-delete' snapcheck='snap-check'
|
||||||
alias snap='snap-create'
|
|
||||||
alias snapls='snap-list'
|
|
||||||
alias snaprm='snap-delete'
|
|
||||||
alias snapshow='snap-show'
|
|
||||||
alias snapcheck='snap-check-limine'
|
|
||||||
alias snapsync='snap-sync'
|
|
||||||
|
|||||||
@@ -1,237 +1,96 @@
|
|||||||
# ============================================================================
|
# ============================================================================
|
||||||
# SSH Session Manager with Tmux Integration
|
# SSH Session Manager with Tmux Integration
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Manage SSH connections with automatic tmux session handling
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
# Source shared colors (with fallback)
|
source "${0:A:h}/../lib/utils.zsh" 2>/dev/null || \
|
||||||
source "${0:A:h}/../lib/colors.zsh" 2>/dev/null || \
|
source "$HOME/.dotfiles/zsh/lib/utils.zsh" 2>/dev/null
|
||||||
source "$HOME/.dotfiles/zsh/lib/colors.zsh" 2>/dev/null || {
|
|
||||||
typeset -g DF_GREEN=$'\033[0;32m' DF_BLUE=$'\033[0;34m'
|
|
||||||
typeset -g DF_YELLOW=$'\033[1;33m' DF_CYAN=$'\033[0;36m'
|
|
||||||
typeset -g DF_RED=$'\033[0;31m' DF_NC=$'\033[0m'
|
|
||||||
}
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Configuration
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
typeset -g SSH_PROFILES_FILE="${SSH_PROFILES_FILE:-$HOME/.dotfiles/.ssh-profiles}"
|
typeset -g SSH_PROFILES_FILE="${SSH_PROFILES_FILE:-$HOME/.dotfiles/.ssh-profiles}"
|
||||||
typeset -g SSH_AUTO_TMUX="${SSH_AUTO_TMUX:-true}"
|
typeset -g SSH_AUTO_TMUX="${SSH_AUTO_TMUX:-true}"
|
||||||
typeset -g SSH_TMUX_SESSION_PREFIX="${SSH_TMUX_SESSION_PREFIX:-ssh}"
|
typeset -g SSH_TMUX_PREFIX="${SSH_TMUX_PREFIX:-ssh}"
|
||||||
typeset -g SSH_SYNC_DOTFILES="${SSH_SYNC_DOTFILES:-ask}"
|
|
||||||
|
|
||||||
# ============================================================================
|
_ssh_init() {
|
||||||
# Helper Functions
|
df_ensure_file "$SSH_PROFILES_FILE" "# SSH Profiles: name|user@host|port|key|options|description"
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
_ssh_print_step() { echo -e "${DF_BLUE}==>${DF_NC} $1"; }
|
|
||||||
_ssh_print_success() { echo -e "${DF_GREEN}✓${DF_NC} $1"; }
|
|
||||||
_ssh_print_error() { echo -e "${DF_RED}✗${DF_NC} $1"; }
|
|
||||||
_ssh_print_info() { echo -e "${DF_CYAN}ℹ${DF_NC} $1"; }
|
|
||||||
|
|
||||||
_ssh_init_profiles() {
|
|
||||||
if [[ ! -f "$SSH_PROFILES_FILE" ]]; then
|
|
||||||
mkdir -p "$(dirname "$SSH_PROFILES_FILE")"
|
|
||||||
cat > "$SSH_PROFILES_FILE" << 'EOF'
|
|
||||||
# SSH Connection Profiles
|
|
||||||
# Format: name|user@host|port|key_file|options|description
|
|
||||||
EOF
|
|
||||||
_ssh_print_success "Created SSH profiles file: $SSH_PROFILES_FILE"
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_ssh_parse_profile() {
|
_ssh_parse() {
|
||||||
local name="$1"
|
local line=$(grep "^${1}|" "$SSH_PROFILES_FILE" 2>/dev/null | head -1)
|
||||||
local line=$(grep "^${name}|" "$SSH_PROFILES_FILE" 2>/dev/null | head -1)
|
|
||||||
[[ -z "$line" ]] && return 1
|
[[ -z "$line" ]] && return 1
|
||||||
IFS='|' read -r profile_name connection port key_file ssh_opts description <<< "$line"
|
echo "$line" | cut -d'|' -f2-
|
||||||
echo "$connection|$port|$key_file|$ssh_opts|$description"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# SSH Profile Management
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
ssh-save() {
|
ssh-save() {
|
||||||
local name="$1" connection="$2" port="${3:-22}" key_file="${4:-}" options="${5:-}" description="${6:-}"
|
local name="$1" conn="$2" port="${3:-22}" key="${4:-}" opts="${5:-}" desc="${6:-}"
|
||||||
|
[[ -z "$name" || -z "$conn" ]] && { echo "Usage: ssh-save <n> <user@host> [port] [key] [opts] [desc]"; return 1; }
|
||||||
_ssh_init_profiles
|
_ssh_init
|
||||||
|
grep -q "^${name}|" "$SSH_PROFILES_FILE" 2>/dev/null && {
|
||||||
[[ -z "$name" || -z "$connection" ]] && {
|
df_confirm "Overwrite '$name'?" || return 1
|
||||||
echo "Usage: ssh-save <name> <user@host> [port] [key_file] [options] [description]"
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
if grep -q "^${name}|" "$SSH_PROFILES_FILE" 2>/dev/null; then
|
|
||||||
echo -e "${DF_YELLOW}⚠${DF_NC} Profile '$name' already exists"
|
|
||||||
read -q "REPLY?Overwrite? [y/N]: "; echo
|
|
||||||
[[ ! "$REPLY" =~ ^[Yy]$ ]] && return 1
|
|
||||||
grep -v "^${name}|" "$SSH_PROFILES_FILE" > "${SSH_PROFILES_FILE}.tmp"
|
grep -v "^${name}|" "$SSH_PROFILES_FILE" > "${SSH_PROFILES_FILE}.tmp"
|
||||||
mv "${SSH_PROFILES_FILE}.tmp" "$SSH_PROFILES_FILE"
|
mv "${SSH_PROFILES_FILE}.tmp" "$SSH_PROFILES_FILE"
|
||||||
fi
|
}
|
||||||
|
echo "${name}|${conn}|${port}|${key}|${opts}|${desc}" >> "$SSH_PROFILES_FILE"
|
||||||
echo "${name}|${connection}|${port}|${key_file}|${options}|${description}" >> "$SSH_PROFILES_FILE"
|
df_print_success "Saved: $name → $conn"
|
||||||
|
|
||||||
_ssh_print_success "Saved SSH profile: $name"
|
|
||||||
echo " Connection: $connection"
|
|
||||||
[[ "$port" != "22" ]] && echo " Port: $port"
|
|
||||||
[[ -n "$key_file" ]] && echo " Key: $key_file"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ssh-list() {
|
ssh-list() {
|
||||||
_ssh_init_profiles
|
_ssh_init
|
||||||
|
df_print_func_name "SSH Profiles"
|
||||||
df_print_func_name "SSH Connection Profiles"
|
local found=false
|
||||||
|
while IFS='|' read -r name conn port key opts desc; do
|
||||||
local has_profiles=false
|
[[ "$name" =~ ^# || -z "$name" ]] && continue
|
||||||
while IFS='|' read -r name connection port key options description; do
|
found=true
|
||||||
[[ "$name" =~ ^# ]] && continue
|
df_print_indent "● $name → $conn"
|
||||||
[[ -z "$name" ]] && continue
|
[[ "$port" != "22" && -n "$port" ]] && df_print_indent " Port: $port"
|
||||||
has_profiles=true
|
[[ -n "$desc" ]] && df_print_indent " $desc"
|
||||||
|
|
||||||
echo -e "${DF_GREEN}●${DF_NC} ${DF_CYAN}$name${DF_NC}"
|
|
||||||
echo " Connection: $connection"
|
|
||||||
[[ "$port" != "22" && -n "$port" ]] && echo " Port: $port"
|
|
||||||
[[ -n "$key" ]] && echo " Key: $key"
|
|
||||||
[[ -n "$description" ]] && echo " Description: $description"
|
|
||||||
echo
|
|
||||||
done < "$SSH_PROFILES_FILE"
|
done < "$SSH_PROFILES_FILE"
|
||||||
|
[[ "$found" != true ]] && df_print_info "No profiles. Use: ssh-save name user@host"
|
||||||
[[ "$has_profiles" != true ]] && {
|
|
||||||
_ssh_print_info "No profiles saved yet"
|
|
||||||
echo "Create a profile with: ssh-save myserver user@example.com"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ssh-delete() {
|
ssh-delete() {
|
||||||
local name="$1"
|
[[ -z "$1" ]] && { echo "Usage: ssh-delete <n>"; return 1; }
|
||||||
[[ -z "$name" ]] && { echo "Usage: ssh-delete <name>"; return 1; }
|
grep -q "^${1}|" "$SSH_PROFILES_FILE" 2>/dev/null || { df_print_error "Not found: $1"; return 1; }
|
||||||
|
grep -v "^${1}|" "$SSH_PROFILES_FILE" > "${SSH_PROFILES_FILE}.tmp"
|
||||||
_ssh_init_profiles
|
|
||||||
|
|
||||||
if ! grep -q "^${name}|" "$SSH_PROFILES_FILE" 2>/dev/null; then
|
|
||||||
_ssh_print_error "Profile '$name' not found"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
grep -v "^${name}|" "$SSH_PROFILES_FILE" > "${SSH_PROFILES_FILE}.tmp"
|
|
||||||
mv "${SSH_PROFILES_FILE}.tmp" "$SSH_PROFILES_FILE"
|
mv "${SSH_PROFILES_FILE}.tmp" "$SSH_PROFILES_FILE"
|
||||||
_ssh_print_success "Deleted profile: $name"
|
df_print_success "Deleted: $1"
|
||||||
}
|
}
|
||||||
|
|
||||||
ssh-connect() {
|
ssh-connect() {
|
||||||
local name="$1"
|
local name="$1" session="${2:-${SSH_TMUX_PREFIX}-${1}}"
|
||||||
local session_name="${2:-${SSH_TMUX_SESSION_PREFIX}-${name}}"
|
[[ -z "$name" ]] && { ssh-list; return 1; }
|
||||||
|
_ssh_init
|
||||||
|
local data=$(_ssh_parse "$name")
|
||||||
|
[[ -z "$data" ]] && { df_print_error "Not found: $name"; return 1; }
|
||||||
|
|
||||||
[[ -z "$name" ]] && { echo "Usage: ssh-connect <profile_name>"; ssh-list; return 1; }
|
IFS='|' read -r conn port key opts desc <<< "$data"
|
||||||
|
df_print_step "Connecting: $name"
|
||||||
|
[[ -n "$desc" ]] && df_print_indent "$desc"
|
||||||
|
|
||||||
_ssh_init_profiles
|
local cmd="ssh"
|
||||||
|
[[ -n "$port" && "$port" != "22" ]] && cmd="$cmd -p $port"
|
||||||
local profile_data=$(_ssh_parse_profile "$name")
|
[[ -n "$key" ]] && cmd="$cmd -i $key"
|
||||||
[[ -z "$profile_data" ]] && { _ssh_print_error "Profile '$name' not found"; return 1; }
|
[[ -n "$opts" ]] && cmd="$cmd $opts"
|
||||||
|
cmd="$cmd $conn"
|
||||||
IFS='|' read -r connection port key_file ssh_opts description <<< "$profile_data"
|
|
||||||
|
|
||||||
_ssh_print_step "Connecting to: $name"
|
|
||||||
[[ -n "$description" ]] && echo " $description"
|
|
||||||
|
|
||||||
local ssh_cmd="ssh"
|
|
||||||
[[ -n "$port" && "$port" != "22" ]] && ssh_cmd="$ssh_cmd -p $port"
|
|
||||||
[[ -n "$key_file" ]] && ssh_cmd="$ssh_cmd -i $key_file"
|
|
||||||
[[ -n "$ssh_opts" ]] && ssh_cmd="$ssh_cmd $ssh_opts"
|
|
||||||
ssh_cmd="$ssh_cmd $connection"
|
|
||||||
|
|
||||||
if [[ "$SSH_AUTO_TMUX" == "true" ]]; then
|
if [[ "$SSH_AUTO_TMUX" == "true" ]]; then
|
||||||
_ssh_print_info "Attaching to tmux session: $session_name"
|
df_print_info "Tmux session: $session"
|
||||||
local tmux_cmd="tmux attach-session -t $session_name 2>/dev/null || tmux new-session -s $session_name"
|
eval "$cmd -t 'tmux attach -t $session 2>/dev/null || tmux new -s $session'"
|
||||||
eval "$ssh_cmd -t '$tmux_cmd'"
|
|
||||||
else
|
else
|
||||||
eval "$ssh_cmd"
|
eval "$cmd"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
sshf() {
|
sshf() {
|
||||||
if ! command -v fzf &>/dev/null; then
|
df_require_cmd fzf || return 1
|
||||||
_ssh_print_error "fzf not installed"
|
_ssh_init
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
_ssh_init_profiles
|
|
||||||
|
|
||||||
local profiles=()
|
local profiles=()
|
||||||
while IFS='|' read -r name connection port key options description; do
|
while IFS='|' read -r name conn port key opts desc; do
|
||||||
[[ "$name" =~ ^# ]] && continue
|
[[ "$name" =~ ^# || -z "$name" ]] && continue
|
||||||
[[ -z "$name" ]] && continue
|
profiles+=("$name|$name → $conn")
|
||||||
local display="$name → $connection"
|
|
||||||
[[ -n "$description" ]] && display="$display ($description)"
|
|
||||||
profiles+=("$name|$display")
|
|
||||||
done < "$SSH_PROFILES_FILE"
|
done < "$SSH_PROFILES_FILE"
|
||||||
|
[[ ${#profiles[@]} -eq 0 ]] && { df_print_info "No profiles"; return 1; }
|
||||||
[[ ${#profiles[@]} -eq 0 ]] && { _ssh_print_info "No profiles saved"; return 1; }
|
local sel=$(printf '%s\n' "${profiles[@]}" | fzf $(df_fzf_opts) --delimiter='|' --with-nth=2 --prompt='SSH > ')
|
||||||
|
[[ -n "$sel" ]] && ssh-connect "${sel%%|*}"
|
||||||
local selection=$(printf '%s\n' "${profiles[@]}" | \
|
|
||||||
fzf --height=50% --layout=reverse --border=rounded --prompt='SSH > ' \
|
|
||||||
--delimiter='|' --with-nth=2)
|
|
||||||
|
|
||||||
[[ -n "$selection" ]] && ssh-connect "${selection%%|*}"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ssh-reconnect() {
|
alias sshl='ssh-list' sshs='ssh-save' sshc='ssh-connect' sshd='ssh-delete'
|
||||||
local name="${1:-last}"
|
_ssh_init
|
||||||
|
|
||||||
if [[ "$name" == "last" ]]; then
|
|
||||||
local last_profile=$(grep "ssh-connect" "$HISTFILE" 2>/dev/null | tail -1 | awk '{print $2}')
|
|
||||||
[[ -z "$last_profile" ]] && { _ssh_print_error "No previous connection found"; return 1; }
|
|
||||||
name="$last_profile"
|
|
||||||
fi
|
|
||||||
|
|
||||||
_ssh_print_info "Reconnecting to: $name"
|
|
||||||
ssh-connect "$name"
|
|
||||||
}
|
|
||||||
|
|
||||||
ssh-sync-dotfiles() {
|
|
||||||
local name="$1"
|
|
||||||
[[ -z "$name" ]] && { echo "Usage: ssh-sync-dotfiles <profile_name>"; return 1; }
|
|
||||||
|
|
||||||
local profile_data=$(_ssh_parse_profile "$name")
|
|
||||||
[[ -z "$profile_data" ]] && { _ssh_print_error "Profile '$name' not found"; return 1; }
|
|
||||||
|
|
||||||
IFS='|' read -r connection port key_file ssh_opts description <<< "$profile_data"
|
|
||||||
|
|
||||||
local dotfiles_dir="${DOTFILES_DIR:-$HOME/.dotfiles}"
|
|
||||||
[[ ! -d "$dotfiles_dir" ]] && { _ssh_print_error "Dotfiles directory not found"; return 1; }
|
|
||||||
|
|
||||||
df_print_func_name "Sync Dotfiles to: $connection"
|
|
||||||
|
|
||||||
local rsync_cmd="rsync -avz --exclude='.git' --exclude='*.local'"
|
|
||||||
[[ -n "$port" && "$port" != "22" ]] && rsync_cmd="$rsync_cmd -e 'ssh -p $port'"
|
|
||||||
[[ -n "$key_file" ]] && rsync_cmd="$rsync_cmd -e 'ssh -i $key_file'"
|
|
||||||
rsync_cmd="$rsync_cmd $dotfiles_dir/ $connection:.dotfiles/"
|
|
||||||
|
|
||||||
_ssh_print_info "Running: $rsync_cmd"
|
|
||||||
|
|
||||||
if eval "$rsync_cmd"; then
|
|
||||||
_ssh_print_success "Dotfiles synced successfully"
|
|
||||||
else
|
|
||||||
_ssh_print_error "Failed to sync dotfiles"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Aliases
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
alias sshl='ssh-list'
|
|
||||||
alias sshs='ssh-save'
|
|
||||||
alias sshc='ssh-connect'
|
|
||||||
alias sshd='ssh-delete'
|
|
||||||
alias sshr='ssh-reconnect'
|
|
||||||
alias sshsync='ssh-sync-dotfiles'
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Initialization
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
_ssh_init_profiles
|
|
||||||
|
|||||||
@@ -1,335 +1,124 @@
|
|||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Systemd Integration for Arch/CachyOS
|
# Systemd Integration for Arch/CachyOS
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Quick shortcuts and helpers for systemd service management
|
|
||||||
#
|
|
||||||
# Commands:
|
|
||||||
# sc <args> - sudo systemctl
|
|
||||||
# scu <args> - systemctl --user
|
|
||||||
# scr <service> - restart and show status
|
|
||||||
# sce <service> - enable and start
|
|
||||||
# scd <service> - disable and stop
|
|
||||||
# sclog <service> - follow journal logs
|
|
||||||
# sc-failed - show failed services
|
|
||||||
# sc-timers - show active timers
|
|
||||||
# sc-recent - recently started services
|
|
||||||
# sc-boot - boot time analysis
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
# Source shared colors (with fallback)
|
source "${0:A:h}/../lib/utils.zsh" 2>/dev/null || \
|
||||||
source "${0:A:h}/../lib/colors.zsh" 2>/dev/null || \
|
source "$HOME/.dotfiles/zsh/lib/utils.zsh" 2>/dev/null
|
||||||
source "$HOME/.dotfiles/zsh/lib/colors.zsh" 2>/dev/null || {
|
|
||||||
typeset -g DF_GREEN=$'\033[0;32m' DF_YELLOW=$'\033[1;33m'
|
|
||||||
typeset -g DF_RED=$'\033[0;31m' DF_BLUE=$'\033[0;34m'
|
|
||||||
typeset -g DF_CYAN=$'\033[0;36m' DF_NC=$'\033[0m'
|
|
||||||
}
|
|
||||||
|
|
||||||
# ============================================================================
|
# Core shortcuts
|
||||||
# Core Systemctl Shortcuts
|
sc() { sudo systemctl "$@"; }
|
||||||
# ============================================================================
|
scu() { systemctl --user "$@"; }
|
||||||
|
|
||||||
# System-level systemctl (with sudo)
|
|
||||||
sc() {
|
|
||||||
sudo systemctl "$@"
|
|
||||||
}
|
|
||||||
|
|
||||||
# User-level systemctl
|
|
||||||
scu() {
|
|
||||||
systemctl --user "$@"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Restart service and show status
|
|
||||||
scr() {
|
scr() {
|
||||||
local service="$1"
|
[[ -z "$1" ]] && { echo "Usage: scr <service>"; return 1; }
|
||||||
[[ -z "$service" ]] && { echo "Usage: scr <service>"; return 1; }
|
df_print_step "Restarting $1..."
|
||||||
|
sudo systemctl restart "$1" && { df_print_success "Restarted"; sudo systemctl status "$1" --no-pager -l; } || df_print_error "Failed"
|
||||||
echo -e "${DF_BLUE}==>${DF_NC} Restarting ${service}..."
|
|
||||||
if sudo systemctl restart "$service"; then
|
|
||||||
echo -e "${DF_GREEN}✓${DF_NC} Restarted successfully"
|
|
||||||
echo ""
|
|
||||||
sudo systemctl status "$service" --no-pager -l
|
|
||||||
else
|
|
||||||
echo -e "${DF_RED}✗${DF_NC} Failed to restart ${service}"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Enable and start service
|
|
||||||
sce() {
|
sce() {
|
||||||
local service="$1"
|
[[ -z "$1" ]] && { echo "Usage: sce <service>"; return 1; }
|
||||||
[[ -z "$service" ]] && { echo "Usage: sce <service>"; return 1; }
|
df_print_step "Enabling $1..."
|
||||||
|
sudo systemctl enable --now "$1" && { df_print_success "Enabled"; sudo systemctl status "$1" --no-pager -l | head -15; } || df_print_error "Failed"
|
||||||
echo -e "${DF_BLUE}==>${DF_NC} Enabling and starting ${service}..."
|
|
||||||
if sudo systemctl enable --now "$service"; then
|
|
||||||
echo -e "${DF_GREEN}✓${DF_NC} ${service} enabled and started"
|
|
||||||
sudo systemctl status "$service" --no-pager -l | head -15
|
|
||||||
else
|
|
||||||
echo -e "${DF_RED}✗${DF_NC} Failed to enable ${service}"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Disable and stop service
|
|
||||||
scd() {
|
scd() {
|
||||||
local service="$1"
|
[[ -z "$1" ]] && { echo "Usage: scd <service>"; return 1; }
|
||||||
[[ -z "$service" ]] && { echo "Usage: scd <service>"; return 1; }
|
df_print_step "Disabling $1..."
|
||||||
|
sudo systemctl disable --now "$1" && df_print_success "Disabled" || df_print_error "Failed"
|
||||||
echo -e "${DF_BLUE}==>${DF_NC} Disabling and stopping ${service}..."
|
|
||||||
if sudo systemctl disable --now "$service"; then
|
|
||||||
echo -e "${DF_GREEN}✓${DF_NC} ${service} disabled and stopped"
|
|
||||||
else
|
|
||||||
echo -e "${DF_RED}✗${DF_NC} Failed to disable ${service}"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Follow journal logs for a service
|
|
||||||
sclog() {
|
sclog() {
|
||||||
local service="$1"
|
[[ -z "$1" ]] && { echo "Usage: sclog <service>"; return 1; }
|
||||||
local lines="${2:-50}"
|
df_print_step "Following logs for $1 (Ctrl+C to exit)..."
|
||||||
[[ -z "$service" ]] && { echo "Usage: sclog <service> [lines]"; return 1; }
|
sudo journalctl -xeu "$1" -f -n "${2:-50}"
|
||||||
|
|
||||||
echo -e "${DF_BLUE}==>${DF_NC} Following logs for ${service} (Ctrl+C to exit)..."
|
|
||||||
sudo journalctl -xeu "$service" -f -n "$lines"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Show recent logs for a service (without follow)
|
|
||||||
sclogs() {
|
sclogs() {
|
||||||
local service="$1"
|
[[ -z "$1" ]] && { echo "Usage: sclogs <service>"; return 1; }
|
||||||
local lines="${2:-50}"
|
sudo journalctl -xeu "$1" -n "${2:-50}" --no-pager
|
||||||
[[ -z "$service" ]] && { echo "Usage: sclogs <service> [lines]"; return 1; }
|
|
||||||
|
|
||||||
sudo journalctl -xeu "$service" -n "$lines" --no-pager
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Service Status Commands
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
# Show failed services (system and user)
|
|
||||||
sc-failed() {
|
sc-failed() {
|
||||||
df_print_func_name "Failed Services"
|
df_print_func_name "Failed Services"
|
||||||
|
df_print_section "System"
|
||||||
echo -e "${DF_CYAN}System Services:${DF_NC}"
|
local sys=$(systemctl --failed --no-pager --no-legend 2>/dev/null)
|
||||||
local sys_failed=$(systemctl --failed --no-pager --no-legend 2>/dev/null)
|
[[ -z "$sys" ]] && df_print_indent "✓ None" || echo "$sys" | sed 's/^/ /'
|
||||||
if [[ -z "$sys_failed" ]]; then
|
|
||||||
echo -e " ${DF_GREEN}✓${DF_NC} No failed system services"
|
|
||||||
else
|
|
||||||
echo "$sys_failed" | sed 's/^/ /'
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo -e "\n${DF_CYAN}User Services:${DF_NC}"
|
|
||||||
local user_failed=$(systemctl --user --failed --no-pager --no-legend 2>/dev/null)
|
|
||||||
if [[ -z "$user_failed" ]]; then
|
|
||||||
echo -e " ${DF_GREEN}✓${DF_NC} No failed user services"
|
|
||||||
else
|
|
||||||
echo "$user_failed" | sed 's/^/ /'
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
|
df_print_section "User"
|
||||||
|
local usr=$(systemctl --user --failed --no-pager --no-legend 2>/dev/null)
|
||||||
|
[[ -z "$usr" ]] && df_print_indent "✓ None" || echo "$usr" | sed 's/^/ /'
|
||||||
}
|
}
|
||||||
|
|
||||||
# Show active timers
|
|
||||||
sc-timers() {
|
sc-timers() {
|
||||||
df_print_func_name "Active Timers"
|
df_print_func_name "Active Timers"
|
||||||
|
df_print_section "System"
|
||||||
echo -e "${DF_CYAN}System Timers:${DF_NC}"
|
systemctl list-timers --no-pager | head -15
|
||||||
systemctl list-timers --no-pager | head -20
|
echo ""
|
||||||
|
df_print_section "User"
|
||||||
echo -e "\n${DF_CYAN}User Timers:${DF_NC}"
|
|
||||||
systemctl --user list-timers --no-pager 2>/dev/null | head -10
|
systemctl --user list-timers --no-pager 2>/dev/null | head -10
|
||||||
|
|
||||||
echo ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Show recently started/stopped services
|
|
||||||
sc-recent() {
|
|
||||||
local count="${1:-15}"
|
|
||||||
|
|
||||||
df_print_func_name "Recent Service Activity"
|
|
||||||
|
|
||||||
echo -e "${DF_CYAN}Recently Started:${DF_NC}"
|
|
||||||
systemctl list-units --type=service --state=running --no-pager --no-legend | \
|
|
||||||
head -"$count" | awk '{print " " $1}'
|
|
||||||
|
|
||||||
echo -e "\n${DF_CYAN}Recent Journal (services):${DF_NC}"
|
|
||||||
journalctl -p 3 -xb --no-pager | tail -"$count" | sed 's/^/ /'
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
}
|
|
||||||
|
|
||||||
# Boot time analysis
|
|
||||||
sc-boot() {
|
sc-boot() {
|
||||||
df_print_func_name "Boot Time Analysis"
|
df_print_func_name "Boot Analysis"
|
||||||
|
df_print_section "Summary"
|
||||||
echo -e "${DF_CYAN}Boot Summary:${DF_NC}"
|
|
||||||
systemd-analyze
|
systemd-analyze
|
||||||
|
echo ""
|
||||||
echo -e "\n${DF_CYAN}Slowest Services (top 10):${DF_NC}"
|
df_print_section "Slowest (top 10)"
|
||||||
systemd-analyze blame --no-pager | head -10 | sed 's/^/ /'
|
systemd-analyze blame --no-pager | head -10 | sed 's/^/ /'
|
||||||
|
|
||||||
echo -e "\n${DF_CYAN}Critical Chain:${DF_NC}"
|
|
||||||
systemd-analyze critical-chain --no-pager 2>/dev/null | head -15 | sed 's/^/ /'
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Service Search and Info
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
# Search for services by name
|
|
||||||
sc-search() {
|
sc-search() {
|
||||||
local query="$1"
|
[[ -z "$1" ]] && { echo "Usage: sc-search <query>"; return 1; }
|
||||||
[[ -z "$query" ]] && { echo "Usage: sc-search <query>"; return 1; }
|
df_print_func_name "Service Search: $1"
|
||||||
|
systemctl list-unit-files --type=service --no-pager | grep -i "$1"
|
||||||
df_print_func_name "Service Search: $query"
|
|
||||||
|
|
||||||
systemctl list-unit-files --type=service --no-pager | grep -i "$query"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Show detailed service info
|
|
||||||
sc-info() {
|
sc-info() {
|
||||||
local service="$1"
|
[[ -z "$1" ]] && { echo "Usage: sc-info <service>"; return 1; }
|
||||||
[[ -z "$service" ]] && { echo "Usage: sc-info <service>"; return 1; }
|
df_print_func_name "Service: $1"
|
||||||
|
systemctl status "$1" --no-pager -l 2>/dev/null || sudo systemctl status "$1" --no-pager -l
|
||||||
df_print_func_name "Service Info: $service"
|
|
||||||
|
|
||||||
echo -e "${DF_CYAN}Status:${DF_NC}"
|
|
||||||
systemctl status "$service" --no-pager -l 2>/dev/null || \
|
|
||||||
sudo systemctl status "$service" --no-pager -l
|
|
||||||
|
|
||||||
echo -e "\n${DF_CYAN}Unit File:${DF_NC}"
|
|
||||||
systemctl cat "$service" 2>/dev/null | head -30
|
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
|
df_print_section "Unit File"
|
||||||
|
systemctl cat "$1" 2>/dev/null | head -30
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================================================
|
# fzf interactive
|
||||||
# Quick Status for MOTD Integration
|
if df_cmd_exists fzf; then
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
# Get count of failed services (for MOTD/prompt)
|
|
||||||
_systemd_failed_count() {
|
|
||||||
local count=$(systemctl --failed --no-pager --no-legend 2>/dev/null | wc -l)
|
|
||||||
echo "$count"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Check if a service is active (for scripts)
|
|
||||||
_systemd_is_active() {
|
|
||||||
local service="$1"
|
|
||||||
systemctl is-active --quiet "$service" 2>/dev/null
|
|
||||||
}
|
|
||||||
|
|
||||||
# Check if a service is enabled (for scripts)
|
|
||||||
_systemd_is_enabled() {
|
|
||||||
local service="$1"
|
|
||||||
systemctl is-enabled --quiet "$service" 2>/dev/null
|
|
||||||
}
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Interactive Service Management (requires fzf)
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
if command -v fzf &>/dev/null; then
|
|
||||||
# Interactive service selector
|
|
||||||
scf() {
|
scf() {
|
||||||
local service=$(systemctl list-units --type=service --no-pager --no-legend | \
|
local svc=$(systemctl list-units --type=service --no-pager --no-legend | \
|
||||||
awk '{print $1, $2, $3, $4}' | \
|
fzf $(df_fzf_opts) --prompt='Service > ' --preview='systemctl status {1} --no-pager' | awk '{print $1}')
|
||||||
fzf --height=50% --layout=reverse --border=rounded \
|
[[ -z "$svc" ]] && return
|
||||||
--prompt='Service > ' \
|
df_print_info "Selected: $svc"
|
||||||
--preview='systemctl status {1} --no-pager' \
|
echo "[s]tatus [r]estart [l]ogs [e]nable [d]isable"
|
||||||
--preview-window=right:50%:wrap | \
|
read -k 1 "act?Action: "; echo
|
||||||
awk '{print $1}')
|
case "$act" in
|
||||||
|
s) sudo systemctl status "$svc" --no-pager -l ;;
|
||||||
if [[ -n "$service" ]]; then
|
r) scr "$svc" ;;
|
||||||
echo -e "${DF_BLUE}Selected:${DF_NC} $service"
|
l) sclog "$svc" ;;
|
||||||
echo ""
|
e) sce "$svc" ;;
|
||||||
echo "Actions: [s]tatus [r]estart [o]stop [l]ogs [e]nable [d]isable [q]uit"
|
d) scd "$svc" ;;
|
||||||
read -k 1 "action?Action: "
|
esac
|
||||||
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"
|
|
||||||
}
|
}
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Aliases
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
alias scs='sc status'
|
|
||||||
alias scstart='sc start'
|
|
||||||
alias scstop='sc stop'
|
|
||||||
alias screload='sc daemon-reload'
|
|
||||||
alias scmask='sc mask'
|
|
||||||
alias scunmask='sc unmask'
|
|
||||||
|
|
||||||
# Journal shortcuts
|
|
||||||
alias jctl='journalctl'
|
|
||||||
alias jctlf='journalctl -f'
|
|
||||||
alias jctlb='journalctl -b'
|
|
||||||
alias jctlerr='journalctl -p err -b'
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Help
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
sc-help() {
|
sc-help() {
|
||||||
df_print_func_name "Systemd Helper Commands"
|
df_print_func_name "Systemd Commands"
|
||||||
|
|
||||||
cat << 'EOF'
|
cat << 'EOF'
|
||||||
|
sc <args> sudo systemctl
|
||||||
Core Commands:
|
scu <args> systemctl --user
|
||||||
sc <args> sudo systemctl <args>
|
scr <svc> Restart + status
|
||||||
scu <args> systemctl --user <args>
|
sce <svc> Enable + start
|
||||||
scr <service> Restart and show status
|
scd <svc> Disable + stop
|
||||||
sce <service> Enable and start (--now)
|
sclog <svc> Follow logs
|
||||||
scd <service> Disable and stop (--now)
|
sclogs <svc> Recent logs
|
||||||
sclog <service> Follow journal logs (-f)
|
sc-failed Failed services
|
||||||
sclogs <service> Show recent logs (no follow)
|
sc-timers Active timers
|
||||||
|
sc-boot Boot analysis
|
||||||
Status Commands:
|
sc-search <q> Search services
|
||||||
sc-failed Show failed services
|
sc-info <svc> Service details
|
||||||
sc-timers Show active timers
|
scf Interactive (fzf)
|
||||||
sc-recent Recently started services
|
|
||||||
sc-boot Boot time analysis
|
|
||||||
sc-search <query> Search services by name
|
|
||||||
sc-info <service> Detailed service info
|
|
||||||
|
|
||||||
Interactive (requires fzf):
|
|
||||||
scf Interactive service manager
|
|
||||||
sclogf Interactive log viewer
|
|
||||||
|
|
||||||
Aliases:
|
|
||||||
scs sc status
|
|
||||||
scstart sc start
|
|
||||||
scstop sc stop
|
|
||||||
screload sc daemon-reload
|
|
||||||
|
|
||||||
Journal:
|
|
||||||
jctl journalctl
|
|
||||||
jctlf journalctl -f
|
|
||||||
jctlb journalctl -b (current boot)
|
|
||||||
jctlerr journalctl -p err -b
|
|
||||||
|
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
|
alias scs='sc status' scstart='sc start' scstop='sc stop' screload='sc daemon-reload'
|
||||||
|
alias jctl='journalctl' jctlf='journalctl -f' jctlb='journalctl -b'
|
||||||
|
|||||||
@@ -1,380 +1,116 @@
|
|||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Tmux Workspace Manager - Project Templates & Layouts
|
# Tmux Workspace Manager - Project Templates & Layouts
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Quick project workspace setup with pre-configured tmux layouts
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
# Source shared colors (with fallback)
|
source "${0:A:h}/../lib/utils.zsh" 2>/dev/null || \
|
||||||
source "${0:A:h}/../lib/colors.zsh" 2>/dev/null || \
|
source "$HOME/.dotfiles/zsh/lib/utils.zsh" 2>/dev/null
|
||||||
source "$HOME/.dotfiles/zsh/lib/colors.zsh" 2>/dev/null || {
|
|
||||||
typeset -g DF_GREEN=$'\033[0;32m' DF_BLUE=$'\033[0;34m'
|
typeset -g TW_TEMPLATES="${TW_TEMPLATES:-$HOME/.dotfiles/.tmux-templates}"
|
||||||
typeset -g DF_YELLOW=$'\033[1;33m' DF_CYAN=$'\033[0;36m'
|
typeset -g TW_PREFIX="${TW_PREFIX:-work}"
|
||||||
typeset -g DF_RED=$'\033[0;31m' DF_NC=$'\033[0m'
|
typeset -g TW_DEFAULT="${TW_DEFAULT:-dev}"
|
||||||
|
|
||||||
|
_tw_check() { df_require_cmd tmux || return 1; }
|
||||||
|
|
||||||
|
_tw_init() {
|
||||||
|
df_ensure_dir "$TW_TEMPLATES"
|
||||||
|
[[ ! -f "$TW_TEMPLATES/dev.tmux" ]] && {
|
||||||
|
echo -e "# Dev layout\nsplit-window -h -p 50\nsplit-window -v -p 50\nselect-pane -t 0" > "$TW_TEMPLATES/dev.tmux"
|
||||||
|
echo -e "# Ops layout\nsplit-window -h\nsplit-window -v\nselect-pane -t 0\nsplit-window -v\nselect-pane -t 0" > "$TW_TEMPLATES/ops.tmux"
|
||||||
|
echo "# Full\n" > "$TW_TEMPLATES/full.tmux"
|
||||||
|
df_print_success "Created default templates"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Configuration
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
typeset -g TW_TEMPLATES_DIR="${TW_TEMPLATES_DIR:-$HOME/.dotfiles/.tmux-templates}"
|
|
||||||
typeset -g TW_SESSION_PREFIX="${TW_SESSION_PREFIX:-work}"
|
|
||||||
typeset -g TW_DEFAULT_TEMPLATE="${TW_DEFAULT_TEMPLATE:-dev}"
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Helper Functions
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
_tw_print_step() { echo -e "${DF_BLUE}==>${DF_NC} $1"; }
|
|
||||||
_tw_print_success() { echo -e "${DF_GREEN}✓${DF_NC} $1"; }
|
|
||||||
_tw_print_error() { echo -e "${DF_RED}✗${DF_NC} $1"; }
|
|
||||||
_tw_print_info() { echo -e "${DF_CYAN}ℹ${DF_NC} $1"; }
|
|
||||||
|
|
||||||
_tw_check_tmux() {
|
|
||||||
if ! command -v tmux &>/dev/null; then
|
|
||||||
_tw_print_error "tmux not installed"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
_tw_init_templates() {
|
|
||||||
mkdir -p "$TW_TEMPLATES_DIR"
|
|
||||||
[[ ! -f "$TW_TEMPLATES_DIR/dev.tmux" ]] && _tw_create_default_templates
|
|
||||||
}
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Default Template Definitions
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
_tw_create_default_templates() {
|
|
||||||
_tw_print_step "Creating default templates..."
|
|
||||||
|
|
||||||
cat > "$TW_TEMPLATES_DIR/dev.tmux" << 'EOF'
|
|
||||||
# Development workspace
|
|
||||||
split-window -h -p 50
|
|
||||||
split-window -v -p 50
|
|
||||||
select-pane -t 0
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat > "$TW_TEMPLATES_DIR/ops.tmux" << 'EOF'
|
|
||||||
# Operations workspace - 4 panes
|
|
||||||
split-window -h -p 50
|
|
||||||
split-window -v -p 50
|
|
||||||
select-pane -t 0
|
|
||||||
split-window -v -p 50
|
|
||||||
select-pane -t 0
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat > "$TW_TEMPLATES_DIR/ssh-multi.tmux" << 'EOF'
|
|
||||||
# Multi-server SSH workspace
|
|
||||||
split-window -h -p 50
|
|
||||||
split-window -v -p 50
|
|
||||||
select-pane -t 0
|
|
||||||
split-window -v -p 50
|
|
||||||
select-pane -t 0
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat > "$TW_TEMPLATES_DIR/debug.tmux" << 'EOF'
|
|
||||||
# Debug workspace
|
|
||||||
split-window -h -p 30
|
|
||||||
select-pane -t 0
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat > "$TW_TEMPLATES_DIR/full.tmux" << 'EOF'
|
|
||||||
# Full workspace - single pane
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat > "$TW_TEMPLATES_DIR/review.tmux" << 'EOF'
|
|
||||||
# Code Review workspace
|
|
||||||
split-window -h -p 50
|
|
||||||
select-pane -t 0
|
|
||||||
EOF
|
|
||||||
|
|
||||||
_tw_print_success "Created default templates in: $TW_TEMPLATES_DIR"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Template Management
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
tw-templates() {
|
tw-templates() {
|
||||||
_tw_init_templates
|
_tw_init
|
||||||
|
df_print_func_name "Tmux Templates"
|
||||||
df_print_func_name "Available Tmux Templates"
|
for t in "$TW_TEMPLATES"/*.tmux; do
|
||||||
|
[[ -f "$t" ]] && df_print_indent "● $(basename "$t" .tmux)"
|
||||||
for template in "$TW_TEMPLATES_DIR"/*.tmux; do
|
|
||||||
[[ ! -f "$template" ]] && continue
|
|
||||||
local name=$(basename "$template" .tmux)
|
|
||||||
local description=$(grep "^#" "$template" | head -2 | tail -1 | sed 's/^# *//')
|
|
||||||
echo -e "${DF_GREEN}●${DF_NC} ${DF_CYAN}$name${DF_NC}"
|
|
||||||
[[ -n "$description" ]] && echo " $description"
|
|
||||||
done
|
done
|
||||||
|
echo ""
|
||||||
echo
|
df_print_info "Create: tw-create <name> <template>"
|
||||||
echo "Create workspace: ${DF_CYAN}tw-create myproject dev${DF_NC}"
|
|
||||||
echo "Quick attach: ${DF_CYAN}tw myproject${DF_NC}"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tw-template-edit() {
|
|
||||||
local template_name="$1"
|
|
||||||
[[ -z "$template_name" ]] && { echo "Usage: tw-template-edit <template_name>"; tw-templates; return 1; }
|
|
||||||
_tw_init_templates
|
|
||||||
${EDITOR:-vim} "$TW_TEMPLATES_DIR/${template_name}.tmux"
|
|
||||||
_tw_print_success "Template edited: $template_name"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Workspace Management
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
tw-create() {
|
tw-create() {
|
||||||
local workspace_name="$1"
|
local name="$1" tmpl="${2:-$TW_DEFAULT}"
|
||||||
local template="${2:-$TW_DEFAULT_TEMPLATE}"
|
[[ -z "$name" ]] && { tw-templates; return 1; }
|
||||||
|
_tw_check || return 1
|
||||||
|
_tw_init
|
||||||
|
|
||||||
[[ -z "$workspace_name" ]] && { echo "Usage: tw-create <workspace_name> [template]"; tw-templates; return 1; }
|
local session="${TW_PREFIX}-${name}"
|
||||||
|
tmux has-session -t "$session" 2>/dev/null && { df_print_error "'$name' exists"; return 1; }
|
||||||
|
|
||||||
_tw_check_tmux || return 1
|
local tfile="$TW_TEMPLATES/${tmpl}.tmux"
|
||||||
_tw_init_templates
|
[[ ! -f "$tfile" ]] && { df_print_error "Template '$tmpl' not found"; tw-templates; return 1; }
|
||||||
|
|
||||||
local session_name="${TW_SESSION_PREFIX}-${workspace_name}"
|
df_print_step "Creating: $name (template: $tmpl)"
|
||||||
|
tmux new-session -d -s "$session"
|
||||||
|
tmux source-file "$tfile" -t "$session"
|
||||||
|
|
||||||
if tmux has-session -t "$session_name" 2>/dev/null; then
|
df_in_git_repo && {
|
||||||
_tw_print_error "Workspace '$workspace_name' already exists"
|
local root=$(df_git_root)
|
||||||
echo "Use: ${DF_CYAN}tw $workspace_name${DF_NC} to attach"
|
df_print_info "Git root: $root"
|
||||||
return 1
|
tmux send-keys -t "$session:0" "cd $root" C-m
|
||||||
fi
|
}
|
||||||
|
|
||||||
local template_file="$TW_TEMPLATES_DIR/${template}.tmux"
|
df_print_success "Created: $name"
|
||||||
if [[ ! -f "$template_file" ]]; then
|
[[ -z "$TMUX" ]] && tmux attach -t "$session" || df_print_info "Switch: tmux switch -t $session"
|
||||||
_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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tw-list() {
|
tw-list() {
|
||||||
_tw_check_tmux || return 1
|
_tw_check || return 1
|
||||||
|
df_print_func_name "Tmux Workspaces"
|
||||||
df_print_func_name "Active Tmux Workspaces"
|
local found=false
|
||||||
|
tmux list-sessions 2>/dev/null | while IFS=: read -r sess rest; do
|
||||||
local has_workspaces=false
|
[[ "$sess" == ${TW_PREFIX}-* ]] && { found=true; df_print_indent "● ${sess#${TW_PREFIX}-}"; }
|
||||||
|
|
||||||
tmux list-sessions 2>/dev/null | while IFS=: read -r session_full rest; do
|
|
||||||
if [[ "$session_full" == ${TW_SESSION_PREFIX}-* ]]; then
|
|
||||||
has_workspaces=true
|
|
||||||
local workspace_name="${session_full#${TW_SESSION_PREFIX}-}"
|
|
||||||
local attached=""
|
|
||||||
|
|
||||||
if [[ -n "$TMUX" ]]; then
|
|
||||||
local current_session=$(tmux display-message -p '#S')
|
|
||||||
[[ "$current_session" == "$session_full" ]] && attached=" ${DF_GREEN}(current)${DF_NC}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo -e "${DF_GREEN}●${DF_NC} ${DF_CYAN}$workspace_name${DF_NC}$attached"
|
|
||||||
echo " Session: $session_full"
|
|
||||||
fi
|
|
||||||
done
|
done
|
||||||
|
[[ "$found" != true ]] && df_print_info "No workspaces. Use: tw-create <name>"
|
||||||
if [[ "$has_workspaces" != true ]]; then
|
}
|
||||||
_tw_print_info "No active workspaces"
|
|
||||||
echo "Create one with: ${DF_CYAN}tw-create myproject${DF_NC}"
|
tw-attach() {
|
||||||
fi
|
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() {
|
tw-delete() {
|
||||||
local workspace_name="$1"
|
[[ -z "$1" ]] && { tw-list; return 1; }
|
||||||
[[ -z "$workspace_name" ]] && { echo "Usage: tw-delete <workspace_name>"; tw-list; return 1; }
|
_tw_check || return 1
|
||||||
|
local session="${TW_PREFIX}-${1}"
|
||||||
_tw_check_tmux || return 1
|
tmux has-session -t "$session" 2>/dev/null || { df_print_error "'$1' not found"; return 1; }
|
||||||
|
tmux kill-session -t "$session"
|
||||||
local session_name="${TW_SESSION_PREFIX}-${workspace_name}"
|
df_print_success "Deleted: $1"
|
||||||
|
|
||||||
if ! tmux has-session -t "$session_name" 2>/dev/null; then
|
|
||||||
_tw_print_error "Workspace '$workspace_name' not found"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
tmux kill-session -t "$session_name"
|
|
||||||
_tw_print_success "Deleted workspace: $workspace_name"
|
|
||||||
}
|
|
||||||
|
|
||||||
tw-save() {
|
|
||||||
local template_name="$1"
|
|
||||||
[[ -z "$template_name" ]] && { echo "Usage: tw-save <template_name>"; return 1; }
|
|
||||||
|
|
||||||
_tw_check_tmux || return 1
|
|
||||||
[[ -z "$TMUX" ]] && { _tw_print_error "Must be run from inside tmux"; return 1; }
|
|
||||||
|
|
||||||
_tw_init_templates
|
|
||||||
|
|
||||||
local template_file="$TW_TEMPLATES_DIR/${template_name}.tmux"
|
|
||||||
[[ -f "$template_file" ]] && {
|
|
||||||
read -q "REPLY?Template '$template_name' exists. Overwrite? [y/N]: "
|
|
||||||
echo
|
|
||||||
[[ ! "$REPLY" =~ ^[Yy]$ ]] && return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
_tw_print_step "Saving current layout as template: $template_name"
|
|
||||||
local pane_count=$(tmux display-message -p '#{window_panes}')
|
|
||||||
|
|
||||||
cat > "$template_file" << EOF
|
|
||||||
# Custom template: $template_name
|
|
||||||
# Saved: $(date)
|
|
||||||
# Panes: $pane_count
|
|
||||||
EOF
|
|
||||||
|
|
||||||
if (( pane_count == 2 )); then
|
|
||||||
echo "split-window -h -p 50" >> "$template_file"
|
|
||||||
elif (( pane_count == 3 )); then
|
|
||||||
echo "split-window -h -p 50" >> "$template_file"
|
|
||||||
echo "split-window -v -p 50" >> "$template_file"
|
|
||||||
elif (( pane_count == 4 )); then
|
|
||||||
echo "split-window -h -p 50" >> "$template_file"
|
|
||||||
echo "split-window -v -p 50" >> "$template_file"
|
|
||||||
echo "select-pane -t 0" >> "$template_file"
|
|
||||||
echo "split-window -v -p 50" >> "$template_file"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "" >> "$template_file"
|
|
||||||
echo "select-pane -t 0" >> "$template_file"
|
|
||||||
|
|
||||||
_tw_print_success "Template saved: $template_name"
|
|
||||||
echo " File: $template_file"
|
|
||||||
echo " Edit: ${DF_CYAN}tw-template-edit $template_name${DF_NC}"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tw() {
|
tw() {
|
||||||
local workspace_name="$1"
|
local name="$1" tmpl="${2:-$TW_DEFAULT}"
|
||||||
local template="${2:-$TW_DEFAULT_TEMPLATE}"
|
[[ -z "$name" ]] && { tw-list; return; }
|
||||||
|
_tw_check || return 1
|
||||||
[[ -z "$workspace_name" ]] && { tw-list; return 0; }
|
local session="${TW_PREFIX}-${name}"
|
||||||
|
tmux has-session -t "$session" 2>/dev/null && tw-attach "$name" || tw-create "$name" "$tmpl"
|
||||||
_tw_check_tmux || return 1
|
|
||||||
|
|
||||||
local session_name="${TW_SESSION_PREFIX}-${workspace_name}"
|
|
||||||
|
|
||||||
if tmux has-session -t "$session_name" 2>/dev/null; then
|
|
||||||
tw-attach "$workspace_name"
|
|
||||||
else
|
|
||||||
_tw_print_info "Workspace doesn't exist. Creating with template: $template"
|
|
||||||
tw-create "$workspace_name" "$template"
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
twf() {
|
twf() {
|
||||||
if ! command -v fzf &>/dev/null; then
|
df_require_cmd fzf || return 1
|
||||||
_tw_print_error "fzf not installed"
|
_tw_check || return 1
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
_tw_check_tmux || return 1
|
|
||||||
|
|
||||||
local sessions=()
|
local sessions=()
|
||||||
tmux list-sessions 2>/dev/null | while IFS=: read -r session_full rest; do
|
tmux list-sessions 2>/dev/null | while IFS=: read -r sess rest; do
|
||||||
if [[ "$session_full" == ${TW_SESSION_PREFIX}-* ]]; then
|
[[ "$sess" == ${TW_PREFIX}-* ]] && sessions+=("${sess#${TW_PREFIX}-}")
|
||||||
local workspace_name="${session_full#${TW_SESSION_PREFIX}-}"
|
|
||||||
sessions+=("$workspace_name")
|
|
||||||
fi
|
|
||||||
done
|
done
|
||||||
|
[[ ${#sessions[@]} -eq 0 ]] && { df_print_info "No workspaces"; return 1; }
|
||||||
[[ ${#sessions[@]} -eq 0 ]] && { _tw_print_info "No workspaces found"; return 1; }
|
local sel=$(printf '%s\n' "${sessions[@]}" | fzf $(df_fzf_opts) --prompt='Workspace > ')
|
||||||
|
[[ -n "$sel" ]] && tw-attach "$sel"
|
||||||
local selection=$(printf '%s\n' "${sessions[@]}" | \
|
|
||||||
fzf --height=40% --layout=reverse --border=rounded --prompt='Workspace > ')
|
|
||||||
|
|
||||||
[[ -n "$selection" ]] && tw-attach "$selection"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tw-sync() {
|
tw-sync() {
|
||||||
[[ -z "$TMUX" ]] && { _tw_print_error "Must be run from inside tmux"; return 1; }
|
[[ -z "$TMUX" ]] && { df_print_error "Must be in tmux"; return 1; }
|
||||||
|
local cur=$(tmux show-window-option -v synchronize-panes 2>/dev/null)
|
||||||
local current=$(tmux show-window-option -v synchronize-panes 2>/dev/null)
|
[[ "$cur" == "on" ]] && { tmux set-window-option synchronize-panes off; df_print_info "Sync: OFF"; } || \
|
||||||
|
{ tmux set-window-option synchronize-panes on; df_print_info "Sync: ON"; }
|
||||||
if [[ "$current" == "on" ]]; then
|
|
||||||
tmux set-window-option synchronize-panes off
|
|
||||||
_tw_print_info "Pane synchronization: ${DF_RED}OFF${DF_NC}"
|
|
||||||
else
|
|
||||||
tmux set-window-option synchronize-panes on
|
|
||||||
_tw_print_info "Pane synchronization: ${DF_GREEN}ON${DF_NC}"
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tw-rename() {
|
alias twl='tw-list' twc='tw-create' twa='tw-attach' twd='tw-delete' twt='tw-templates'
|
||||||
local old_name="$1"
|
_tw_init
|
||||||
local new_name="$2"
|
|
||||||
|
|
||||||
[[ -z "$old_name" || -z "$new_name" ]] && { echo "Usage: tw-rename <old_name> <new_name>"; return 1; }
|
|
||||||
|
|
||||||
_tw_check_tmux || return 1
|
|
||||||
|
|
||||||
local old_session="${TW_SESSION_PREFIX}-${old_name}"
|
|
||||||
local new_session="${TW_SESSION_PREFIX}-${new_name}"
|
|
||||||
|
|
||||||
if ! tmux has-session -t "$old_session" 2>/dev/null; then
|
|
||||||
_tw_print_error "Workspace '$old_name' not found"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
tmux rename-session -t "$old_session" "$new_session"
|
|
||||||
_tw_print_success "Renamed: $old_name → $new_name"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Aliases
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
alias twl='tw-list'
|
|
||||||
alias twc='tw-create'
|
|
||||||
alias twa='tw-attach'
|
|
||||||
alias twd='tw-delete'
|
|
||||||
alias tws='tw-save'
|
|
||||||
alias twt='tw-templates'
|
|
||||||
alias twe='tw-template-edit'
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Initialization
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
_tw_init_templates
|
|
||||||
|
|||||||
@@ -2,114 +2,52 @@
|
|||||||
# Shared Utility Functions for Zsh Dotfiles
|
# Shared Utility Functions for Zsh Dotfiles
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Common helper functions used across multiple function files
|
# Common helper functions used across multiple function files
|
||||||
|
# Note: colors.zsh provides: DF_* color variables and df_print_func_name
|
||||||
#
|
#
|
||||||
# Source this file in your .zshrc or in individual function files:
|
# Source this file in function files:
|
||||||
# source "$HOME/.dotfiles/zsh/lib/utils.zsh"
|
# source "${0:A:h}/../lib/utils.zsh"
|
||||||
#
|
|
||||||
# Provides:
|
|
||||||
# - Standardized output formatting (step/success/error/warning/info)
|
|
||||||
# - Command dependency checking
|
|
||||||
# - User confirmation prompts
|
|
||||||
# - Common file/directory operations
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
# Ensure colors are loaded first
|
# Ensure colors are loaded first (provides DF_* vars and df_print_func_name)
|
||||||
source "${0:A:h}/colors.zsh" 2>/dev/null || {
|
source "${0:A:h}/colors.zsh" 2>/dev/null || \
|
||||||
typeset -g DF_GREEN=$'\033[0;32m' DF_YELLOW=$'\033[1;33m'
|
source "$HOME/.dotfiles/zsh/lib/colors.zsh" 2>/dev/null
|
||||||
typeset -g DF_RED=$'\033[0;31m' DF_BLUE=$'\033[0;34m'
|
|
||||||
typeset -g DF_CYAN=$'\033[0;36m' DF_DIM=$'\033[2m' DF_NC=$'\033[0m'
|
|
||||||
}
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Output Formatting Functions
|
# Output Formatting Functions
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# These provide consistent, styled output across all dotfiles functions.
|
|
||||||
# Use these instead of raw echo statements for a unified look.
|
|
||||||
|
|
||||||
# Print a step/action indicator (blue arrow)
|
# Print a step/action indicator (blue arrow)
|
||||||
# Usage: df_print_step "Installing packages"
|
df_print_step() { echo -e "${DF_BLUE}==>${DF_NC} $1"; }
|
||||||
df_print_step() {
|
|
||||||
echo -e "${DF_BLUE}==>${DF_NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Print a success message (green checkmark)
|
# Print a success message (green checkmark)
|
||||||
# Usage: df_print_success "Installation complete"
|
df_print_success() { echo -e "${DF_GREEN}✓${DF_NC} $1"; }
|
||||||
df_print_success() {
|
|
||||||
echo -e "${DF_GREEN}✓${DF_NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Print an error message (red X)
|
# Print an error message (red X)
|
||||||
# Usage: df_print_error "Failed to connect"
|
df_print_error() { echo -e "${DF_RED}✗${DF_NC} $1"; }
|
||||||
df_print_error() {
|
|
||||||
echo -e "${DF_RED}✗${DF_NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Print a warning message (yellow warning sign)
|
# Print a warning message (yellow warning sign)
|
||||||
# Usage: df_print_warning "Config file missing, using defaults"
|
df_print_warning() { echo -e "${DF_YELLOW}⚠${DF_NC} $1"; }
|
||||||
df_print_warning() {
|
|
||||||
echo -e "${DF_YELLOW}⚠${DF_NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Print an info message (cyan info icon)
|
# Print an info message (cyan info icon)
|
||||||
# Usage: df_print_info "Using cached version"
|
df_print_info() { echo -e "${DF_CYAN}ℹ${DF_NC} $1"; }
|
||||||
df_print_info() {
|
|
||||||
echo -e "${DF_CYAN}ℹ${DF_NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Print a section header (cyan, for grouping output)
|
# Print a section header (cyan label)
|
||||||
# Usage: df_print_section "Configuration"
|
df_print_section() { echo -e "${DF_CYAN}$1:${DF_NC}"; }
|
||||||
df_print_section() {
|
|
||||||
echo -e "${DF_CYAN}$1:${DF_NC}"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Print a bullet point item (green dot)
|
# Print indented content
|
||||||
# Usage: df_print_item "my-alias" "git status"
|
df_print_indent() { echo " $1"; }
|
||||||
df_print_item() {
|
|
||||||
local name="$1"
|
|
||||||
local description="${2:-}"
|
|
||||||
if [[ -n "$description" ]]; then
|
|
||||||
echo -e " ${DF_GREEN}●${DF_NC} ${DF_CYAN}$name${DF_NC} - $description"
|
|
||||||
else
|
|
||||||
echo -e " ${DF_GREEN}●${DF_NC} ${DF_CYAN}$name${DF_NC}"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Print indented content (for sub-items)
|
|
||||||
# Usage: df_print_indent "Additional details here"
|
|
||||||
df_print_indent() {
|
|
||||||
echo " $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Print a function name header (box style) - already defined in colors.zsh
|
|
||||||
# but ensure it's available
|
|
||||||
if ! typeset -f df_print_func_name &>/dev/null; then
|
|
||||||
df_print_func_name() {
|
|
||||||
local name="$1"
|
|
||||||
local width=$((${#name} + 4))
|
|
||||||
local border=$(printf '─%.0s' $(seq 1 $width))
|
|
||||||
echo -e "${DF_CYAN}╭${border}╮${DF_NC}"
|
|
||||||
echo -e "${DF_CYAN}│${DF_NC} $name ${DF_CYAN}│${DF_NC}"
|
|
||||||
echo -e "${DF_CYAN}╰${border}╯${DF_NC}"
|
|
||||||
}
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Command Dependency Checking
|
# Command Dependency Checking
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Check if required commands are available before running functions.
|
|
||||||
|
|
||||||
# Check if a command exists
|
# Check if a command exists
|
||||||
# Usage: df_cmd_exists git && echo "git is installed"
|
df_cmd_exists() { command -v "$1" &>/dev/null; }
|
||||||
df_cmd_exists() {
|
|
||||||
command -v "$1" &>/dev/null
|
|
||||||
}
|
|
||||||
|
|
||||||
# Require a command, exit with error if missing
|
# Require a command, show install hint if missing
|
||||||
# Usage: df_require_cmd fzf || return 1
|
|
||||||
# Usage: df_require_cmd compsize "compsize" || return 1
|
|
||||||
df_require_cmd() {
|
df_require_cmd() {
|
||||||
local cmd="$1"
|
local cmd="$1"
|
||||||
local package="${2:-$1}" # Default to command name as package name
|
local package="${2:-$1}"
|
||||||
|
|
||||||
if ! command -v "$cmd" &>/dev/null; then
|
if ! command -v "$cmd" &>/dev/null; then
|
||||||
df_print_error "$cmd not installed"
|
df_print_error "$cmd not installed"
|
||||||
@@ -119,139 +57,55 @@ df_require_cmd() {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
# Require multiple commands at once
|
|
||||||
# Usage: df_require_cmds git fzf tmux || return 1
|
|
||||||
df_require_cmds() {
|
|
||||||
local missing=()
|
|
||||||
for cmd in "$@"; do
|
|
||||||
if ! command -v "$cmd" &>/dev/null; then
|
|
||||||
missing+=("$cmd")
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ ${#missing[@]} -gt 0 ]]; then
|
|
||||||
df_print_error "Missing required commands: ${missing[*]}"
|
|
||||||
echo "Install: sudo pacman -S ${missing[*]}"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# User Confirmation Prompts
|
# User Confirmation
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Consistent confirmation dialogs for dangerous operations.
|
|
||||||
|
|
||||||
# Ask for yes/no confirmation (defaults to no)
|
# Ask for yes/no confirmation (defaults to no)
|
||||||
# Usage: df_confirm "Delete all files?" || return
|
|
||||||
df_confirm() {
|
df_confirm() {
|
||||||
local prompt="$1"
|
local prompt="$1"
|
||||||
read -q "REPLY?$prompt [y/N]: "
|
read -q "REPLY?$prompt [y/N]: "
|
||||||
echo # Newline after response
|
echo
|
||||||
[[ "$REPLY" =~ ^[Yy]$ ]]
|
[[ "$REPLY" =~ ^[Yy]$ ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
# Ask for confirmation with a warning prefix
|
# Confirm with warning prefix
|
||||||
# Usage: df_confirm_warning "This will delete all data" || return
|
|
||||||
df_confirm_warning() {
|
df_confirm_warning() {
|
||||||
local message="$1"
|
df_print_warning "$1"
|
||||||
df_print_warning "$message"
|
|
||||||
df_confirm "Continue?"
|
df_confirm "Continue?"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# File and Directory Helpers
|
# File/Directory Helpers
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
# Check if running inside a git repository
|
# Check if in a git repo
|
||||||
# Usage: df_in_git_repo && echo "In a git repo"
|
df_in_git_repo() { git rev-parse --git-dir &>/dev/null 2>&1; }
|
||||||
df_in_git_repo() {
|
|
||||||
git rev-parse --git-dir &>/dev/null 2>&1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Get git root directory
|
# Get git root directory
|
||||||
# Usage: local root=$(df_git_root)
|
df_git_root() { git rev-parse --show-toplevel 2>/dev/null; }
|
||||||
df_git_root() {
|
|
||||||
git rev-parse --show-toplevel 2>/dev/null
|
|
||||||
}
|
|
||||||
|
|
||||||
# Ensure a directory exists, create if missing
|
# Ensure directory exists
|
||||||
# Usage: df_ensure_dir "$HOME/.cache/myapp"
|
df_ensure_dir() { [[ ! -d "$1" ]] && mkdir -p "$1"; }
|
||||||
df_ensure_dir() {
|
|
||||||
local dir="$1"
|
|
||||||
[[ ! -d "$dir" ]] && mkdir -p "$dir"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Ensure a file exists with optional default content
|
# Ensure file exists with optional default content
|
||||||
# Usage: df_ensure_file "$HOME/.config/myapp/config" "# Default config"
|
|
||||||
df_ensure_file() {
|
df_ensure_file() {
|
||||||
local file="$1"
|
local file="$1" content="${2:-}"
|
||||||
local default_content="${2:-}"
|
|
||||||
|
|
||||||
if [[ ! -f "$file" ]]; then
|
if [[ ! -f "$file" ]]; then
|
||||||
df_ensure_dir "$(dirname "$file")"
|
df_ensure_dir "$(dirname "$file")"
|
||||||
if [[ -n "$default_content" ]]; then
|
[[ -n "$content" ]] && echo "$content" > "$file" || touch "$file"
|
||||||
echo "$default_content" > "$file"
|
|
||||||
else
|
|
||||||
touch "$file"
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# String Helpers
|
# Environment Checks
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
# Truncate a string to a maximum length
|
df_in_tmux() { [[ -n "$TMUX" ]]; }
|
||||||
# Usage: df_truncate "Long string here" 10 # "Long st..."
|
df_is_btrfs() { [[ "$(df -T / 2>/dev/null | awk 'NR==2 {print $2}')" == "btrfs" ]]; }
|
||||||
df_truncate() {
|
|
||||||
local str="$1"
|
|
||||||
local max="${2:-50}"
|
|
||||||
|
|
||||||
if [[ ${#str} -gt $max ]]; then
|
|
||||||
echo "${str:0:$((max-3))}..."
|
|
||||||
else
|
|
||||||
echo "$str"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Pad a string to a minimum length
|
|
||||||
# Usage: df_pad "short" 10 # "short "
|
|
||||||
df_pad() {
|
|
||||||
local str="$1"
|
|
||||||
local width="${2:-20}"
|
|
||||||
printf "%-${width}s" "$str"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# FZF Helpers
|
# FZF Helpers
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
# Standard fzf options for consistent look
|
df_fzf_opts() { echo "--height=50% --layout=reverse --border=rounded"; }
|
||||||
df_fzf_opts() {
|
|
||||||
echo "--height=50% --layout=reverse --border=rounded"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Run fzf with standard options
|
|
||||||
# Usage: echo -e "opt1\nopt2" | df_fzf "Select > "
|
|
||||||
df_fzf() {
|
|
||||||
local prompt="${1:-Select > }"
|
|
||||||
shift
|
|
||||||
fzf $(df_fzf_opts) --prompt="$prompt" "$@"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Environment Helpers
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
# Check if inside tmux
|
|
||||||
# Usage: df_in_tmux && echo "Inside tmux"
|
|
||||||
df_in_tmux() {
|
|
||||||
[[ -n "$TMUX" ]]
|
|
||||||
}
|
|
||||||
|
|
||||||
# Check if root filesystem is btrfs
|
|
||||||
# Usage: df_is_btrfs && echo "Using btrfs"
|
|
||||||
df_is_btrfs() {
|
|
||||||
[[ "$(df -T / | awk 'NR==2 {print $2}')" == "btrfs" ]]
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user