diff --git a/zsh/functions/btrfs-helpers.zsh b/zsh/functions/btrfs-helpers.zsh index 7450a01..3088b58 100644 --- a/zsh/functions/btrfs-helpers.zsh +++ b/zsh/functions/btrfs-helpers.zsh @@ -2,429 +2,209 @@ # Btrfs Helpers for Arch/CachyOS # ============================================================================ # Quick commands for btrfs filesystem management -# CachyOS defaults to btrfs, so these are highly useful -# -# Commands: -# btrfs-usage - Show filesystem usage -# btrfs-subs - List subvolumes -# btrfs-balance - Start balance operation -# btrfs-scrub - Start/check scrub -# btrfs-defrag - Defragment file or directory -# btrfs-compress - Show compression stats -# btrfs-info - Full filesystem info -# btrfs-health - Quick health check # ============================================================================ -# Source shared colors (with fallback) -source "${0:A:h}/../lib/colors.zsh" 2>/dev/null || \ -source "$HOME/.dotfiles/zsh/lib/colors.zsh" 2>/dev/null || { - typeset -g DF_GREEN=$'\033[0;32m' DF_YELLOW=$'\033[1;33m' - typeset -g DF_RED=$'\033[0;31m' DF_BLUE=$'\033[0;34m' - typeset -g DF_CYAN=$'\033[0;36m' DF_NC=$'\033[0m' -} - -# ============================================================================ -# Configuration -# ============================================================================ +source "${0:A:h}/../lib/utils.zsh" 2>/dev/null || \ +source "$HOME/.dotfiles/zsh/lib/utils.zsh" 2>/dev/null typeset -g BTRFS_DEFAULT_MOUNT="${BTRFS_DEFAULT_MOUNT:-/}" -# ============================================================================ -# Detection -# ============================================================================ - _btrfs_check() { - if ! command -v btrfs &>/dev/null; then - echo -e "${DF_RED}✗${DF_NC} btrfs-progs not installed" - echo "Install: sudo pacman -S btrfs-progs" + df_require_cmd btrfs btrfs-progs || return 1 + if ! df_is_btrfs; then + df_print_warning "Root filesystem is not btrfs" return 1 fi - - # Check if root is btrfs - local fstype=$(df -T / | awk 'NR==2 {print $2}') - if [[ "$fstype" != "btrfs" ]]; then - echo -e "${DF_YELLOW}⚠${DF_NC} Root filesystem is not btrfs (detected: $fstype)" - return 1 - fi - return 0 } -# ============================================================================ -# Core Commands -# ============================================================================ - -# Show filesystem usage btrfs-usage() { _btrfs_check || return 1 local mount="${1:-$BTRFS_DEFAULT_MOUNT}" - df_print_func_name "Btrfs Filesystem Usage: ${mount}" - sudo btrfs filesystem usage "$mount" -h } -# List all subvolumes btrfs-subs() { _btrfs_check || return 1 local mount="${1:-$BTRFS_DEFAULT_MOUNT}" - df_print_func_name "Btrfs Subvolumes" - - echo -e "${DF_CYAN}Subvolume List:${DF_NC}" + df_print_section "Subvolume List" sudo btrfs subvolume list "$mount" | while read -r line; do local path=$(echo "$line" | awk '{print $NF}') local id=$(echo "$line" | awk '{print $2}') - echo -e " ${DF_GREEN}●${DF_NC} [$id] $path" + df_print_indent "● [$id] $path" done - echo "" - echo -e "${DF_CYAN}Default Subvolume:${DF_NC}" + df_print_section "Default Subvolume" sudo btrfs subvolume get-default "$mount" } -# Start balance operation btrfs-balance() { _btrfs_check || return 1 local mount="${1:-$BTRFS_DEFAULT_MOUNT}" - local usage="${2:-50}" # Default: rebalance chunks with <50% usage - + local usage="${2:-50}" df_print_func_name "Btrfs Balance" - - echo -e "${DF_YELLOW}⚠${DF_NC} This may take a while and use significant I/O" + df_confirm_warning "This may take a while and use significant I/O" || return 0 echo "" - - read -q "REPLY?Continue? [y/N]: "; echo - [[ ! "$REPLY" =~ ^[Yy]$ ]] && return 0 - - echo "" - echo -e "${DF_BLUE}==>${DF_NC} Balancing data chunks with <${usage}% usage..." + df_print_step "Balancing data chunks with <${usage}% usage..." sudo btrfs balance start -dusage="$usage" -musage="$usage" "$mount" -v - - if [[ $? -eq 0 ]]; then - echo -e "${DF_GREEN}✓${DF_NC} Balance completed" - else - echo -e "${DF_YELLOW}⚠${DF_NC} Balance finished (may have been interrupted or had no work)" - fi + [[ $? -eq 0 ]] && df_print_success "Balance completed" || df_print_warning "Balance finished (may have been interrupted)" } -# Check balance status btrfs-balance-status() { _btrfs_check || return 1 - local mount="${1:-$BTRFS_DEFAULT_MOUNT}" - df_print_func_name "Btrfs Balance Status" - - sudo btrfs balance status "$mount" + sudo btrfs balance status "${1:-$BTRFS_DEFAULT_MOUNT}" } -# Cancel running balance btrfs-balance-cancel() { _btrfs_check || return 1 - local mount="${1:-$BTRFS_DEFAULT_MOUNT}" - - echo -e "${DF_BLUE}==>${DF_NC} Cancelling balance on ${mount}..." - sudo btrfs balance cancel "$mount" + df_print_step "Cancelling balance..." + sudo btrfs balance cancel "${1:-$BTRFS_DEFAULT_MOUNT}" } -# Start scrub operation btrfs-scrub() { _btrfs_check || return 1 local mount="${1:-$BTRFS_DEFAULT_MOUNT}" - df_print_func_name "Btrfs Scrub" - - # Check if scrub is already running local status=$(sudo btrfs scrub status "$mount" 2>/dev/null) if echo "$status" | grep -q "running"; then - echo -e "${DF_CYAN}Scrub Status (running):${DF_NC}" + df_print_section "Scrub Status (running)" echo "$status" | sed 's/^/ /' return 0 fi - - echo -e "${DF_YELLOW}⚠${DF_NC} Scrub verifies data integrity and may take hours" - read -q "REPLY?Start scrub? [y/N]: "; echo - [[ ! "$REPLY" =~ ^[Yy]$ ]] && return 0 - - echo "" - echo -e "${DF_BLUE}==>${DF_NC} Starting scrub..." + df_print_warning "Scrub verifies data integrity and may take hours" + df_confirm "Start scrub?" || return 0 + df_print_step "Starting scrub..." sudo btrfs scrub start "$mount" - echo "" - echo -e "${DF_CYAN}Scrub Status:${DF_NC}" + df_print_section "Scrub Status" sudo btrfs scrub status "$mount" - - echo "" - echo -e "${DF_CYAN}Monitor with:${DF_NC} btrfs-scrub-status" + df_print_info "Monitor with: btrfs-scrub-status" } -# Show scrub status btrfs-scrub-status() { _btrfs_check || return 1 - local mount="${1:-$BTRFS_DEFAULT_MOUNT}" - df_print_func_name "Btrfs Scrub Status" - - sudo btrfs scrub status "$mount" + sudo btrfs scrub status "${1:-$BTRFS_DEFAULT_MOUNT}" } -# Cancel scrub btrfs-scrub-cancel() { _btrfs_check || return 1 - local mount="${1:-$BTRFS_DEFAULT_MOUNT}" - - echo -e "${DF_BLUE}==>${DF_NC} Cancelling scrub on ${mount}..." - sudo btrfs scrub cancel "$mount" + df_print_step "Cancelling scrub..." + sudo btrfs scrub cancel "${1:-$BTRFS_DEFAULT_MOUNT}" } -# Defragment file or directory btrfs-defrag() { _btrfs_check || return 1 local target="${1:-.}" - - if [[ ! -e "$target" ]]; then - echo -e "${DF_RED}✗${DF_NC} Target not found: $target" - return 1 - fi - + [[ ! -e "$target" ]] && { df_print_error "Target not found: $target"; return 1; } df_print_func_name "Btrfs Defragment" - if [[ -d "$target" ]]; then - echo -e "${DF_YELLOW}⚠${DF_NC} Recursive defrag on directory: $target" - read -q "REPLY?Continue? [y/N]: "; echo - [[ ! "$REPLY" =~ ^[Yy]$ ]] && return 0 - + df_print_warning "Recursive defrag on directory: $target" + df_confirm "Continue?" || return 0 sudo btrfs filesystem defragment -r -v "$target" else - echo -e "${DF_BLUE}==>${DF_NC} Defragmenting: $target" + df_print_step "Defragmenting: $target" sudo btrfs filesystem defragment -v "$target" fi - - echo -e "${DF_GREEN}✓${DF_NC} Defragmentation complete" + df_print_success "Defragmentation complete" } -# Show compression stats (requires compsize) btrfs-compress() { _btrfs_check || return 1 - local target="${1:-$BTRFS_DEFAULT_MOUNT}" - - if ! command -v compsize &>/dev/null; then - echo -e "${DF_YELLOW}⚠${DF_NC} compsize not installed" - echo "Install: sudo pacman -S compsize" - return 1 - fi - + df_require_cmd compsize || return 1 df_print_func_name "Btrfs Compression Statistics" - - sudo compsize "$target" + sudo compsize "${1:-$BTRFS_DEFAULT_MOUNT}" } -# ============================================================================ -# Information Commands -# ============================================================================ - -# Full filesystem info btrfs-info() { _btrfs_check || return 1 local mount="${1:-$BTRFS_DEFAULT_MOUNT}" - df_print_func_name "Btrfs Filesystem Information" - - echo -e "${DF_CYAN}Filesystem Show:${DF_NC}" + df_print_section "Filesystem Show" sudo btrfs filesystem show "$mount" - - echo -e "\n${DF_CYAN}Filesystem df:${DF_NC}" - sudo btrfs filesystem df "$mount" - - echo -e "\n${DF_CYAN}Device Stats:${DF_NC}" - sudo btrfs device stats "$mount" - echo "" + df_print_section "Filesystem df" + sudo btrfs filesystem df "$mount" + echo "" + df_print_section "Device Stats" + sudo btrfs device stats "$mount" } -# Quick health check btrfs-health() { _btrfs_check || return 1 local mount="${1:-$BTRFS_DEFAULT_MOUNT}" - df_print_func_name "Btrfs Health Check" - local issues=0 - # Check device stats for errors - echo -e "${DF_CYAN}Device Errors:${DF_NC}" - local stats=$(sudo btrfs device stats "$mount" 2>/dev/null) - local errors=$(echo "$stats" | grep -v " 0$" | grep -v "^$") - - if [[ -z "$errors" ]]; then - echo -e " ${DF_GREEN}✓${DF_NC} No device errors detected" - else - echo -e " ${DF_RED}✗${DF_NC} Errors detected:" - echo "$errors" | sed 's/^/ /' - issues=$((issues + 1)) - fi - - # Check allocation - echo -e "\n${DF_CYAN}Space Allocation:${DF_NC}" - local usage=$(sudo btrfs filesystem usage "$mount" -b 2>/dev/null) - local used_pct=$(echo "$usage" | grep "Used:" | head -1 | awk '{print $2}' | tr -d '%') + df_print_section "Device Errors" + local errors=$(sudo btrfs device stats "$mount" 2>/dev/null | grep -v " 0$" | grep -v "^$") + [[ -z "$errors" ]] && df_print_indent "✓ No errors" || { df_print_indent "✗ Errors detected:"; echo "$errors" | sed 's/^/ /'; ((issues++)); } + echo "" + df_print_section "Space Allocation" + local used_pct=$(sudo btrfs filesystem usage "$mount" -b 2>/dev/null | grep "Used:" | head -1 | awk '{print $2}' | tr -d '%') if [[ -n "$used_pct" ]]; then - if (( used_pct >= 90 )); then - echo -e " ${DF_RED}✗${DF_NC} Filesystem ${used_pct}% full - critical!" - issues=$((issues + 1)) - elif (( used_pct >= 80 )); then - echo -e " ${DF_YELLOW}⚠${DF_NC} Filesystem ${used_pct}% full - consider cleanup" - else - echo -e " ${DF_GREEN}✓${DF_NC} Filesystem ${used_pct}% used" - fi - fi - - # Check last scrub - echo -e "\n${DF_CYAN}Last Scrub:${DF_NC}" - local scrub_status=$(sudo btrfs scrub status "$mount" 2>/dev/null) - local scrub_date=$(echo "$scrub_status" | grep "Scrub started" | awk '{print $3, $4, $5}') - local scrub_errors=$(echo "$scrub_status" | grep "Error summary" | grep -v "no errors") - - if [[ -n "$scrub_date" ]]; then - echo -e " Last scrub: $scrub_date" - if [[ -n "$scrub_errors" ]]; then - echo -e " ${DF_RED}✗${DF_NC} Scrub found errors" - echo "$scrub_errors" | sed 's/^/ /' - issues=$((issues + 1)) - else - echo -e " ${DF_GREEN}✓${DF_NC} No errors in last scrub" - fi - else - echo -e " ${DF_YELLOW}⚠${DF_NC} No scrub has been run (recommended monthly)" - fi - - # Summary - echo "" - if (( issues == 0 )); then - echo -e "${DF_GREEN}✓${DF_NC} Btrfs filesystem appears healthy" - else - echo -e "${DF_RED}✗${DF_NC} Found $issues issue(s) - investigate above" + (( used_pct >= 90 )) && { df_print_indent "✗ ${used_pct}% full - critical!"; ((issues++)); } || \ + (( used_pct >= 80 )) && df_print_indent "⚠ ${used_pct}% full" || df_print_indent "✓ ${used_pct}% used" fi echo "" + df_print_section "Last Scrub" + local scrub=$(sudo btrfs scrub status "$mount" 2>/dev/null) + local scrub_date=$(echo "$scrub" | grep "Scrub started" | awk '{print $3, $4, $5}') + [[ -n "$scrub_date" ]] && df_print_indent "Last: $scrub_date" || df_print_indent "⚠ No scrub run yet" + + echo "" + (( issues == 0 )) && df_print_success "Filesystem healthy" || df_print_error "Found $issues issue(s)" } -# ============================================================================ -# Snapshot Helpers (complement snapper.zsh) -# ============================================================================ - -# Show snapshot space usage btrfs-snap-usage() { _btrfs_check || return 1 - df_print_func_name "Snapshot Disk Space Usage" - if [[ -d "/.snapshots" ]]; then - echo -e "${DF_CYAN}Snapshot Directory:${DF_NC}" - local size - size=$(sudo du -sh /.snapshots 2>/dev/null | cut -f1) - if [[ -n "$size" ]]; then - echo " $size" - else - echo " Unable to calculate (timeout or error)" - fi - - echo -e "\n${DF_CYAN}Individual Snapshots (top 10 by size):${DF_NC}" - sudo du -sh /.snapshots/*/ 2>/dev/null | sort -h | tail -10 | sed 's/^/ /' || \ - echo " Unable to list snapshots" + df_print_section "Snapshot Directory" + local size=$(timeout 10 sudo du -sh /.snapshots 2>/dev/null | cut -f1) + df_print_indent "${size:-Unable to calculate}" + echo "" + df_print_section "Individual Snapshots (top 10)" + timeout 30 sudo du -sh /.snapshots/*/ 2>/dev/null | sort -h | tail -10 | sed 's/^/ /' else - echo -e "${DF_YELLOW}⚠${DF_NC} No /.snapshots directory found" + df_print_warning "No /.snapshots directory found" fi - - echo "" } -# ============================================================================ -# Maintenance -# ============================================================================ - -# Full maintenance routine btrfs-maintain() { _btrfs_check || return 1 local mount="${1:-$BTRFS_DEFAULT_MOUNT}" - df_print_func_name "Btrfs Maintenance Routine" - - echo "This will perform:" - echo " 1. Health check" - echo " 2. Balance (low usage chunks)" - echo " 3. Scrub (data integrity)" - echo "" - echo -e "${DF_YELLOW}⚠${DF_NC} This may take several hours" - read -q "REPLY?Continue? [y/N]: "; echo - [[ ! "$REPLY" =~ ^[Yy]$ ]] && return 0 - - echo "" - echo -e "${DF_BLUE}==>${DF_NC} Step 1/3: Health Check" + echo "This will: health check, balance, scrub" + df_confirm_warning "This may take several hours" || return 0 + df_print_step "Step 1/3: Health Check" btrfs-health "$mount" - - echo -e "${DF_BLUE}==>${DF_NC} Step 2/3: Balance" + df_print_step "Step 2/3: Balance" sudo btrfs balance start -dusage=50 -musage=50 "$mount" - - echo "" - echo -e "${DF_BLUE}==>${DF_NC} Step 3/3: Scrub" - sudo btrfs scrub start -B "$mount" # -B runs in foreground - - echo "" - echo -e "${DF_GREEN}✓${DF_NC} Maintenance complete" - btrfs-health "$mount" + df_print_step "Step 3/3: Scrub" + sudo btrfs scrub start -B "$mount" + df_print_success "Maintenance complete" } -# ============================================================================ -# Aliases -# ============================================================================ - -alias btru='btrfs-usage' -alias btrs='btrfs-subs' -alias btrh='btrfs-health' -alias btri='btrfs-info' -alias btrc='btrfs-compress' - -# ============================================================================ -# Help -# ============================================================================ - btrfs-help() { df_print_func_name "Btrfs Helper Commands" - cat << 'EOF' - - Information: - btrfs-usage [mount] Filesystem usage summary - btrfs-subs [mount] List all subvolumes - btrfs-info [mount] Full filesystem information - btrfs-health [mount] Quick health check - btrfs-compress [path] Compression statistics (requires compsize) - - Maintenance: - btrfs-balance [mount] Start balance operation - btrfs-balance-status Check balance progress - btrfs-balance-cancel Cancel running balance - btrfs-scrub [mount] Start scrub (integrity check) - btrfs-scrub-status Check scrub progress - btrfs-scrub-cancel Cancel running scrub - btrfs-defrag Defragment file/directory - btrfs-maintain [mount] Full maintenance routine - - Snapshots: - btrfs-snap-usage Show snapshot space usage - - Aliases: - btru btrfs-usage - btrs btrfs-subs - btrh btrfs-health - btri btrfs-info - btrc btrfs-compress - - Note: Most commands default to / if no mount point specified. - - See also: snapper.zsh for snapshot management - + btrfs-usage [mount] Filesystem usage + btrfs-subs [mount] List subvolumes + btrfs-info [mount] Full filesystem info + btrfs-health [mount] Quick health check + btrfs-compress [path] Compression stats + btrfs-balance [mount] Start balance + btrfs-scrub [mount] Start scrub + btrfs-defrag Defragment + btrfs-snap-usage Snapshot space usage + btrfs-maintain [mount] Full maintenance EOF } + +alias btru='btrfs-usage' btrs='btrfs-subs' btrh='btrfs-health' btri='btrfs-info' btrc='btrfs-compress' diff --git a/zsh/functions/command-palette.zsh b/zsh/functions/command-palette.zsh index 129fba5..57ef146 100644 --- a/zsh/functions/command-palette.zsh +++ b/zsh/functions/command-palette.zsh @@ -2,322 +2,135 @@ # Command Palette - Fuzzy Command Launcher for Zsh # ============================================================================ # A Raycast/Alfred-style command palette for the terminal -# -# Features: -# - Search aliases, functions, recent commands -# - Search bookmarked directories -# - Search dotfiles scripts -# - Quick actions (edit config, reload shell, etc.) -# # Keybinding: Ctrl+Space (configurable) -# -# Requirements: fzf # ============================================================================ -# Source shared colors (with fallback) -source "${0:A:h}/../lib/colors.zsh" 2>/dev/null || \ -source "$HOME/.dotfiles/zsh/lib/colors.zsh" 2>/dev/null || { - typeset -g DF_GREEN=$'\033[0;32m' DF_BLUE=$'\033[0;34m' - typeset -g DF_CYAN=$'\033[0;36m' DF_NC=$'\033[0m' -} +source "${0:A:h}/../lib/utils.zsh" 2>/dev/null || \ +source "$HOME/.dotfiles/zsh/lib/utils.zsh" 2>/dev/null -# ============================================================================ -# Configuration -# ============================================================================ - -typeset -g PALETTE_HOTKEY="${PALETTE_HOTKEY:-^@}" # Ctrl+Space +typeset -g PALETTE_HOTKEY="${PALETTE_HOTKEY:-^@}" typeset -g PALETTE_HISTORY_SIZE=50 typeset -g PALETTE_BOOKMARKS_FILE="$HOME/.dotfiles/.bookmarks" typeset -g DOTFILES_DIR="${DOTFILES_DIR:-$HOME/.dotfiles}" -# Icons (works with most terminals) -typeset -g ICON_ALIAS="⚡" -typeset -g ICON_FUNC="λ" -typeset -g ICON_HIST="↺" -typeset -g ICON_DIR="📁" -typeset -g ICON_SCRIPT="⚙" -typeset -g ICON_ACTION="★" -typeset -g ICON_GIT="⎇" -typeset -g ICON_DOCKER="◉" -typeset -g ICON_EDIT="✎" -typeset -g ICON_RUN="▶" - -# ============================================================================ -# Check Dependencies -# ============================================================================ - -_palette_check_deps() { - if ! command -v fzf &>/dev/null; then - echo "Command palette requires fzf." - echo "Install: git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf && ~/.fzf/install" - return 1 - fi - return 0 -} - -# ============================================================================ -# Data Sources -# ============================================================================ +typeset -g ICON_ALIAS="⚡" ICON_FUNC="λ" ICON_HIST="↺" ICON_DIR="📁" +typeset -g ICON_SCRIPT="⚙" ICON_ACTION="★" ICON_GIT="⎇" _palette_get_aliases() { - alias | sed 's/^alias //' | while IFS='=' read -r alias_name cmd; do - cmd="${cmd#\'}" - cmd="${cmd%\'}" - cmd="${cmd#\"}" - cmd="${cmd%\"}" - printf "%s\t%s\t%s\t%s\n" "$ICON_ALIAS" "alias" "$alias_name" "$cmd" + alias | sed 's/^alias //' | while IFS='=' read -r name cmd; do + cmd="${cmd#\'}"; cmd="${cmd%\'}"; cmd="${cmd#\"}"; cmd="${cmd%\"}" + printf "%s\t%s\t%s\t%s\n" "$ICON_ALIAS" "alias" "$name" "$cmd" done } _palette_get_functions() { - print -l ${(ok)functions} | grep -v '^_' | while read -r func_name; do - printf "%s\t%s\t%s\t%s\n" "$ICON_FUNC" "func" "$func_name" "function" + print -l ${(ok)functions} | grep -v "^_" | while read -r name; do + printf "%s\t%s\t%s\t%s\n" "$ICON_FUNC" "function" "$name" "" done } _palette_get_history() { - fc -ln -$PALETTE_HISTORY_SIZE | tac | awk '!seen[$0]++' | head -30 | while read -r cmd; do - [[ -n "$cmd" ]] && printf "%s\t%s\t%s\t%s\n" "$ICON_HIST" "history" "${cmd:0:50}" "$cmd" + fc -ln -"$PALETTE_HISTORY_SIZE" 2>/dev/null | awk '!seen[$0]++' | while read -r cmd; do + [[ -n "$cmd" ]] && printf "%s\t%s\t%s\t%s\n" "$ICON_HIST" "history" "$cmd" "" done } _palette_get_bookmarks() { [[ ! -f "$PALETTE_BOOKMARKS_FILE" ]] && return - - while IFS='|' read -r bm_name bm_path; do - [[ -n "$bm_name" && -n "$bm_path" ]] && printf "%s\t%s\t%s\t%s\n" "$ICON_DIR" "bookmark" "$bm_name" "cd $bm_path" + while IFS='|' read -r name path desc; do + [[ "$name" =~ ^# || -z "$name" ]] && continue + printf "%s\t%s\t%s\t%s\n" "$ICON_DIR" "bookmark" "$name" "$path" done < "$PALETTE_BOOKMARKS_FILE" } -_palette_get_scripts() { - [[ ! -d "$DOTFILES_DIR/bin" ]] && return - - for script in "$DOTFILES_DIR/bin"/*.sh; do - [[ -f "$script" ]] || continue - local script_name=$(basename "$script" .sh) - printf "%s\t%s\t%s\t%s\n" "$ICON_SCRIPT" "script" "$script_name" "$script" - done -} - -_palette_get_git_commands() { - git rev-parse --git-dir &>/dev/null || return - - local branch=$(git branch --show-current 2>/dev/null) - - printf "%s\t%s\t%s\t%s\n" "$ICON_GIT" "git" "status" "git status" - printf "%s\t%s\t%s\t%s\n" "$ICON_GIT" "git" "pull $branch" "git pull origin $branch" - printf "%s\t%s\t%s\t%s\n" "$ICON_GIT" "git" "push $branch" "git push origin $branch" - printf "%s\t%s\t%s\t%s\n" "$ICON_GIT" "git" "diff" "git diff" - printf "%s\t%s\t%s\t%s\n" "$ICON_GIT" "git" "log" "git log --oneline -20" - printf "%s\t%s\t%s\t%s\n" "$ICON_GIT" "git" "stash" "git stash" - printf "%s\t%s\t%s\t%s\n" "$ICON_GIT" "git" "stash pop" "git stash pop" -} - -_palette_get_docker_commands() { - command -v docker &>/dev/null || return - - printf "%s\t%s\t%s\t%s\n" "$ICON_DOCKER" "docker" "ps" "docker ps" - printf "%s\t%s\t%s\t%s\n" "$ICON_DOCKER" "docker" "ps -a" "docker ps -a" - printf "%s\t%s\t%s\t%s\n" "$ICON_DOCKER" "docker" "images" "docker images" - printf "%s\t%s\t%s\t%s\n" "$ICON_DOCKER" "docker" "compose up" "docker-compose up -d" - printf "%s\t%s\t%s\t%s\n" "$ICON_DOCKER" "docker" "compose down" "docker-compose down" - printf "%s\t%s\t%s\t%s\n" "$ICON_DOCKER" "docker" "prune" "docker system prune -af" -} - _palette_get_actions() { - printf "%s\t%s\t%s\t%s\n" "$ICON_ACTION" "action" "Reload shell" "exec zsh" - printf "%s\t%s\t%s\t%s\n" "$ICON_EDIT" "action" "Edit .zshrc" "${EDITOR:-vim} ~/.zshrc" - printf "%s\t%s\t%s\t%s\n" "$ICON_EDIT" "action" "Edit dotfiles.conf" "${EDITOR:-vim} $DOTFILES_DIR/dotfiles.conf" - printf "%s\t%s\t%s\t%s\n" "$ICON_EDIT" "action" "Edit theme" "${EDITOR:-vim} $DOTFILES_DIR/zsh/themes/adlee.zsh-theme" - printf "%s\t%s\t%s\t%s\n" "$ICON_SCRIPT" "action" "Dotfiles doctor" "dfd" - printf "%s\t%s\t%s\t%s\n" "$ICON_SCRIPT" "action" "Dotfiles sync" "dfs" - printf "%s\t%s\t%s\t%s\n" "$ICON_SCRIPT" "action" "Shell stats" "dfstats" - printf "%s\t%s\t%s\t%s\n" "$ICON_SCRIPT" "action" "Compile zsh" "dfcompile" - printf "%s\t%s\t%s\t%s\n" "$ICON_SCRIPT" "action" "Vault list" "vault list" - printf "%s\t%s\t%s\t%s\n" "$ICON_ACTION" "action" "Clear screen" "clear" - printf "%s\t%s\t%s\t%s\n" "$ICON_DIR" "action" "Home" "cd ~" - printf "%s\t%s\t%s\t%s\n" "$ICON_DIR" "action" "Dotfiles" "cd $DOTFILES_DIR" - printf "%s\t%s\t%s\t%s\n" "$ICON_DIR" "action" "Projects" "cd ~/projects 2>/dev/null || cd ~" + cat << 'EOF' +★ action reload-shell Reload zsh configuration +★ action edit-zshrc Edit ~/.zshrc +★ action dotfiles-update Update dotfiles +EOF + df_in_git_repo && cat << 'EOF' +⎇ git git-status Show git status +⎇ git git-pull Pull latest +⎇ git git-push Push commits +EOF } -_palette_get_directories() { - dirs -v 2>/dev/null | tail -n +2 | head -10 | while read -r num dir; do - [[ -n "$dir" ]] && printf "%s\t%s\t%s\t%s\n" "$ICON_DIR" "recent" "$dir" "cd $dir" - done -} - -# ============================================================================ -# Main Palette Function -# ============================================================================ - -_palette_generate_entries() { - _palette_get_actions - _palette_get_git_commands - _palette_get_docker_commands - _palette_get_aliases - _palette_get_bookmarks - _palette_get_scripts - _palette_get_directories - _palette_get_history - _palette_get_functions -} - -command_palette() { - _palette_check_deps || return 1 - - local selection - selection=$(_palette_generate_entries | \ - fzf --height=60% \ - --layout=reverse \ - --border=rounded \ - --prompt='❯ ' \ - --pointer='▶' \ - --header='Command Palette (ESC to cancel)' \ - --preview-window=hidden \ - --delimiter=$'\t' \ - --with-nth=1,3 \ - --tabstop=2 \ - --ansi \ - --bind='ctrl-r:reload(_palette_generate_entries)' \ - --expect=ctrl-e,ctrl-y) - - [[ -z "$selection" ]] && return - - local key=$(echo "$selection" | head -1) - local line=$(echo "$selection" | tail -1) - local cmd=$(echo "$line" | cut -f4) - - [[ -z "$cmd" ]] && return - - case "$key" in - ctrl-e) - print -z "$cmd" - ;; - ctrl-y) - echo -n "$cmd" | pbcopy 2>/dev/null || echo -n "$cmd" | xclip -selection clipboard 2>/dev/null - echo "Copied: $cmd" - ;; - *) - echo "❯ $cmd" - eval "$cmd" - ;; +_palette_run_action() { + case "$1" in + reload-shell) source ~/.zshrc; df_print_success "Shell reloaded" ;; + edit-zshrc) ${EDITOR:-vim} ~/.zshrc ;; + dotfiles-update) cd "$DOTFILES_DIR" && git pull ;; + git-status) git status ;; + git-pull) git pull ;; + git-push) git push ;; + *) df_print_error "Unknown action: $1" ;; esac } -# Alias for easier access -palette() { command_palette; } -p() { command_palette; } - -# ============================================================================ -# Bookmark Management -# ============================================================================ +palette() { + df_require_cmd fzf || return 1 + local items=$(_palette_get_actions; _palette_get_aliases; _palette_get_functions; _palette_get_bookmarks; _palette_get_history) + local sel=$(echo "$items" | fzf --ansi --delimiter='\t' --with-nth=1,3,4 $(df_fzf_opts) --prompt='> ') + [[ -z "$sel" ]] && return + local type=$(echo "$sel" | cut -f2) name=$(echo "$sel" | cut -f3) detail=$(echo "$sel" | cut -f4) + case "$type" in + alias|history) print -z "$name" ;; + function) print -z "$name " ;; + bookmark) cd "$detail" && pwd ;; + action|git) _palette_run_action "$name" ;; + esac +} bookmark() { - local bm_name="$1" - local bm_path="${2:-$(pwd)}" - - # Ensure bookmarks file parent directory exists - mkdir -p "$(dirname "$PALETTE_BOOKMARKS_FILE")" 2>/dev/null - - # Create bookmarks file if it doesn't exist - [[ ! -f "$PALETTE_BOOKMARKS_FILE" ]] && touch "$PALETTE_BOOKMARKS_FILE" - - if [[ -z "$bm_name" ]]; then - echo "Usage: bookmark [path]" - echo " bookmark list" - echo " bookmark delete " - return 1 - fi - - case "$bm_name" in - list|ls) - df_print_func_name "Bookmarks" - if [[ -s "$PALETTE_BOOKMARKS_FILE" ]]; then - while IFS='|' read -r stored_name stored_path || [[ -n "$stored_name" ]]; do - [[ -n "$stored_name" ]] && echo -e " ${DF_GREEN}●${DF_NC} $stored_name → $stored_path" - done < "$PALETTE_BOOKMARKS_FILE" - else - echo "No bookmarks yet" - fi + local cmd="${1:-list}"; shift 2>/dev/null + case "$cmd" in + add) + local name="$1" path="${2:-$(pwd)}" desc="$3" + [[ -z "$name" ]] && { echo "Usage: bookmark add [path]"; return 1; } + df_ensure_file "$PALETTE_BOOKMARKS_FILE" "# Bookmarks: name|path|description" + grep -q "^${name}|" "$PALETTE_BOOKMARKS_FILE" 2>/dev/null && { + df_confirm "Overwrite '$name'?" || return 1 + grep -v "^${name}|" "$PALETTE_BOOKMARKS_FILE" > "${PALETTE_BOOKMARKS_FILE}.tmp" + mv "${PALETTE_BOOKMARKS_FILE}.tmp" "$PALETTE_BOOKMARKS_FILE" + } + echo "${name}|${path}|${desc}" >> "$PALETTE_BOOKMARKS_FILE" + df_print_success "Bookmarked: $name → $path" ;; delete|rm) - local to_delete="$2" - if [[ -z "$to_delete" ]]; then - echo "Specify bookmark to delete" - return 1 - fi - if [[ -s "$PALETTE_BOOKMARKS_FILE" ]]; then - # Use a temp file approach that won't hang - local temp_file="${PALETTE_BOOKMARKS_FILE}.tmp.$$" - grep -v "^${to_delete}|" "$PALETTE_BOOKMARKS_FILE" > "$temp_file" 2>/dev/null || true - mv -f "$temp_file" "$PALETTE_BOOKMARKS_FILE" - echo -e "${DF_GREEN}✓${DF_NC} Deleted: $to_delete" - else - echo "No bookmarks to delete" - fi + [[ -z "$1" ]] && { echo "Usage: bookmark delete "; return 1; } + grep -q "^${1}|" "$PALETTE_BOOKMARKS_FILE" 2>/dev/null || { df_print_error "Not found: $1"; return 1; } + grep -v "^${1}|" "$PALETTE_BOOKMARKS_FILE" > "${PALETTE_BOOKMARKS_FILE}.tmp" + mv "${PALETTE_BOOKMARKS_FILE}.tmp" "$PALETTE_BOOKMARKS_FILE" + df_print_success "Deleted: $1" ;; - *) - # Remove existing bookmark with same name (if file has content) - if [[ -s "$PALETTE_BOOKMARKS_FILE" ]]; then - local temp_file="${PALETTE_BOOKMARKS_FILE}.tmp.$$" - grep -v "^${bm_name}|" "$PALETTE_BOOKMARKS_FILE" > "$temp_file" 2>/dev/null || true - mv -f "$temp_file" "$PALETTE_BOOKMARKS_FILE" - fi - # Add new bookmark - echo "${bm_name}|${bm_path}" >> "$PALETTE_BOOKMARKS_FILE" - echo -e "${DF_GREEN}✓${DF_NC} Bookmarked: $bm_name → $bm_path" + list|ls) + df_print_func_name "Bookmarks" + [[ ! -f "$PALETTE_BOOKMARKS_FILE" ]] && { df_print_info "No bookmarks"; return; } + while IFS='|' read -r name path desc; do + [[ "$name" =~ ^# || -z "$name" ]] && continue + df_print_indent "● $name → $path" + done < "$PALETTE_BOOKMARKS_FILE" ;; + go) + [[ -z "$1" ]] && { echo "Usage: bookmark go "; 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 " ;; esac } -# Quick jump to bookmark -jump() { - local bm_name="$1" - - if [[ -z "$bm_name" ]]; then - # Fuzzy select bookmark - if [[ ! -s "$PALETTE_BOOKMARKS_FILE" ]]; then - echo "No bookmarks" - return 1 - fi - - local selection=$(cat "$PALETTE_BOOKMARKS_FILE" | \ - fzf --height=40% --layout=reverse --delimiter='|' --with-nth=1 \ - --preview='echo "Path: $(echo {} | cut -d"|" -f2)"') - - if [[ -n "$selection" ]]; then - local jump_path=$(echo "$selection" | cut -d'|' -f2) - cd "$jump_path" && echo "→ $jump_path" - fi - else - # Direct jump - local jump_path=$(grep "^${bm_name}|" "$PALETTE_BOOKMARKS_FILE" 2>/dev/null | cut -d'|' -f2) - if [[ -n "$jump_path" ]]; then - cd "$jump_path" && echo "→ $jump_path" - else - echo "Bookmark not found: $bm_name" - fi - fi +bm() { + df_require_cmd fzf || return 1 + [[ ! -f "$PALETTE_BOOKMARKS_FILE" ]] && { df_print_info "No bookmarks"; return 1; } + local sel=$(grep -v "^#" "$PALETTE_BOOKMARKS_FILE" | grep -v "^$" | \ + fzf $(df_fzf_opts) --delimiter='|' --with-nth=1,2 --prompt='Bookmark > ') + [[ -n "$sel" ]] && cd "$(echo "$sel" | cut -d'|' -f2)" } -# Aliases -bm() { bookmark "$@"; } -j() { jump "$@"; } - -# ============================================================================ -# Widget for Keybinding -# ============================================================================ - -_palette_widget() { - command_palette - zle reset-prompt -} - -# Register widget +_palette_widget() { BUFFER=""; zle redisplay; palette; zle reset-prompt; } zle -N _palette_widget - -# Bind to Ctrl+Space (^@) bindkey "$PALETTE_HOTKEY" _palette_widget -# Alternative binding: Ctrl+P -bindkey '^P' _palette_widget +alias p='palette' bml='bookmark list' bma='bookmark add' bmg='bookmark go' diff --git a/zsh/functions/password-manager.zsh b/zsh/functions/password-manager.zsh index 6ebc91b..4778aea 100644 --- a/zsh/functions/password-manager.zsh +++ b/zsh/functions/password-manager.zsh @@ -1,198 +1,84 @@ # ============================================================================ -# Password Manager Integration for Zsh (LastPass Only) -# ============================================================================ -# Unified interface for LastPass CLI -# -# Usage: -# pw list # List all items -# pw get # Get password -# pw otp # Get OTP/TOTP code -# pw search # Search items -# pw copy # Copy password to clipboard +# Password Manager Integration (LastPass CLI) # ============================================================================ -# Source shared colors (with fallback) -source "${0:A:h}/../lib/colors.zsh" 2>/dev/null || \ -source "$HOME/.dotfiles/zsh/lib/colors.zsh" 2>/dev/null || { - typeset -g DF_GREEN=$'\033[0;32m' DF_BLUE=$'\033[0;34m' - typeset -g DF_YELLOW=$'\033[1;33m' DF_CYAN=$'\033[0;36m' - typeset -g DF_RED=$'\033[0;31m' DF_NC=$'\033[0m' -} +source "${0:A:h}/../lib/utils.zsh" 2>/dev/null || \ +source "$HOME/.dotfiles/zsh/lib/utils.zsh" 2>/dev/null -# ============================================================================ -# LastPass Functions -# ============================================================================ +typeset -g PW_CLIP_TIME="${PW_CLIP_TIME:-45}" -_lp_ensure_session() { +_pw_check() { + df_require_cmd lpass lastpass-cli || return 1 if ! lpass status -q 2>/dev/null; then - echo "Signing into LastPass..." >&2 - lpass login "${LASTPASS_EMAIL:-}" + df_print_warning "Not logged in" + df_print_step "Logging in..." + lpass login --trust "${LPASS_USER:-}" || { df_print_error "Login failed"; return 1; } fi } -_lp_list() { - _lp_ensure_session - lpass ls --format="%an\t%ag" 2>/dev/null -} - -_lp_get() { - local item="$1" - local field="${2:-password}" - _lp_ensure_session - - case "$field" in - password) lpass show --password "$item" 2>/dev/null ;; - username) lpass show --username "$item" 2>/dev/null ;; - url) lpass show --url "$item" 2>/dev/null ;; - notes) lpass show --notes "$item" 2>/dev/null ;; - *) lpass show --field="$field" "$item" 2>/dev/null ;; - esac -} - -_lp_otp() { - local item="$1" - _lp_ensure_session - lpass show --otp "$item" 2>/dev/null -} - -_lp_search() { - local query="$1" - _lp_ensure_session - lpass ls 2>/dev/null | grep -i "$query" -} - -# ============================================================================ -# Unified Interface -# ============================================================================ - -pw() { - local cmd="${1:-help}" - shift - - if ! command -v lpass &>/dev/null; then - echo -e "${DF_RED}✗${DF_NC} LastPass CLI (lpass) not installed" - echo "Install with: yay -S lastpass-cli" +_pw_copy() { + local text="$1" label="${2:-Password}" + if df_cmd_exists wl-copy; then + echo -n "$text" | wl-copy + elif df_cmd_exists xclip; then + echo -n "$text" | xclip -selection clipboard + else + df_print_error "No clipboard tool (install wl-clipboard or xclip)" return 1 fi - + df_print_success "$label copied (clears in ${PW_CLIP_TIME}s)" + (sleep "$PW_CLIP_TIME" && { wl-copy "" 2>/dev/null || xclip -selection clipboard < /dev/null 2>/dev/null; }) & +} + +pw() { + local cmd="${1:-search}" case "$cmd" in - list|ls|l) - df_print_func_name "LastPass Vault" - _lp_list + login) lpass login --trust "${LPASS_USER:-}" ;; + logout) lpass logout -f; df_print_success "Logged out" ;; + sync) _pw_check || return 1; df_print_step "Syncing..."; lpass sync; df_print_success "Synced" ;; + show) _pw_check || return 1; [[ -z "$2" ]] && { echo "Usage: pw show "; return 1; }; lpass show "$2" ;; + gen|generate) + local len="${2:-20}" + local pass=$(tr -dc 'A-Za-z0-9!@#$%^&*' < /dev/urandom | head -c "$len") + _pw_copy "$pass" "Generated password" ;; - - get|g|show) - local item="$1" - local field="${2:-password}" - [[ -z "$item" ]] && { echo "Usage: pw get [field]"; return 1; } - _lp_get "$item" "$field" - ;; - - otp|totp|2fa) - local item="$1" - [[ -z "$item" ]] && { echo "Usage: pw otp "; return 1; } - _lp_otp "$item" - ;; - - search|find|s) - local query="$1" - [[ -z "$query" ]] && { echo "Usage: pw search "; 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 [field]"; return 1; } - - local value=$(_lp_get "$item" "$field") - - if [[ -n "$value" ]]; then - echo -n "$value" | xclip -selection clipboard 2>/dev/null || \ - echo -n "$value" | xsel --clipboard 2>/dev/null || \ - { echo "Could not copy to clipboard"; return 1; } - echo -e "${DF_GREEN}✓${DF_NC} Copied to clipboard" + list|ls) _pw_check || return 1; df_print_func_name "Password Entries"; lpass ls --long ;; + search|*) + _pw_check || return 1 + local query="$1"; [[ "$cmd" == "search" ]] && query="$2" + if df_cmd_exists fzf && [[ -z "$query" ]]; then + local entry=$(lpass ls --format "%an (%au) [%ai]" 2>/dev/null | fzf $(df_fzf_opts) --prompt='Password > ') + [[ -z "$entry" ]] && return + local id=$(echo "$entry" | grep -oP '\[\K[^\]]+(?=\]$)') + local pass=$(lpass show --password "$id" 2>/dev/null) + [[ -n "$pass" ]] && _pw_copy "$pass" || df_print_error "Could not retrieve" else - echo -e "${DF_RED}✗${DF_NC} Item not found or empty" - return 1 + [[ -z "$query" ]] && { echo "Usage: pw "; return 1; } + local results=$(lpass ls --format "%an [%ai]" 2>/dev/null | grep -i "$query") + local count=$(echo "$results" | grep -c . 2>/dev/null || echo 0) + if (( count == 0 )); then + df_print_warning "No entries for: $query" + elif (( count == 1 )); then + local id=$(echo "$results" | grep -oP '\[\K[^\]]+(?=\]$)') + _pw_copy "$(lpass show --password "$id" 2>/dev/null)" + else + df_print_warning "Multiple entries:" + echo "$results" | while read -r l; do df_print_indent "$l"; done + fi fi ;; - - lock) - lpass logout -f 2>/dev/null - echo -e "${DF_GREEN}✓${DF_NC} Logged out of LastPass" - ;; - - help|--help|-h|*) - df_print_func_name "Password Manager CLI" - echo "Usage: pw [args]" - echo - echo "Commands:" - echo " list List all items" - echo " get [field] Get field (default: password)" - echo " otp Get OTP/TOTP code" - echo " search Search items" - echo " copy [field] Copy to clipboard" - echo " lock Logout/lock session" - echo " help Show this help" - echo - echo "Fields: password, username, url, notes, or custom field name" - echo - echo "Examples:" - echo " pw get github" - echo " pw get github username" - echo " pw otp github" - echo " pw copy aws" - echo " pw search mail" - echo - echo "Install: yay -S lastpass-cli" + help|--help|-h) + df_print_func_name "Password Manager" + cat << 'EOF' + pw Search and copy password + pw show Show entry details + pw list List all entries + pw gen [len] Generate password (default: 20) + pw sync Sync vault + pw login/logout Auth commands +EOF ;; esac } -# ============================================================================ -# Aliases -# ============================================================================ - -alias pwl='pw list' -alias pwg='pw get' -alias pwc='pw copy' -alias pws='pw search' - -# ============================================================================ -# FZF Integration (if available) -# ============================================================================ - -if command -v fzf &>/dev/null; then - pwf() { - if ! command -v lpass &>/dev/null; then - echo "LastPass CLI not installed" - return 1 - fi - - local item=$(_lp_list | fzf --height=40% --reverse | cut -f1) - - if [[ -n "$item" ]]; then - pw copy "$item" - fi - } - - pwof() { - if ! command -v lpass &>/dev/null; then - echo "LastPass CLI not installed" - return 1 - fi - - local item=$(_lp_list | fzf --height=40% --reverse | cut -f1) - - if [[ -n "$item" ]]; then - local otp=$(pw otp "$item") - if [[ -n "$otp" ]]; then - echo -n "$otp" | xclip -selection clipboard 2>/dev/null || \ - echo -n "$otp" | xsel --clipboard 2>/dev/null - echo -e "${DF_GREEN}✓${DF_NC} OTP copied: $otp" - fi - fi - } -fi +alias pwc='pw' pws='pw show' pwg='pw gen' pwl='pw list' diff --git a/zsh/functions/python-templates.zsh b/zsh/functions/python-templates.zsh index c464203..038ef12 100644 --- a/zsh/functions/python-templates.zsh +++ b/zsh/functions/python-templates.zsh @@ -1,488 +1,173 @@ # ============================================================================ # Python Project Template Functions # ============================================================================ -# Quick project scaffolding with virtual environments -# -# Usage: -# py-new # Create new Python project -# py-django # Create Django project -# py-flask # Create Flask project -# py-fastapi # Create FastAPI project -# py-data # Create data science project -# py-cli # Create CLI tool project -# ============================================================================ -# Source shared colors (with fallback) -source "${0:A:h}/../lib/colors.zsh" 2>/dev/null || \ -source "$HOME/.dotfiles/zsh/lib/colors.zsh" 2>/dev/null || { - typeset -g DF_GREEN=$'\033[0;32m' DF_BLUE=$'\033[0;34m' - typeset -g DF_YELLOW=$'\033[1;33m' DF_CYAN=$'\033[0;36m' - typeset -g DF_RED=$'\033[0;31m' DF_NC=$'\033[0m' -} +source "${0:A:h}/../lib/utils.zsh" 2>/dev/null || \ +source "$HOME/.dotfiles/zsh/lib/utils.zsh" 2>/dev/null -# ============================================================================ -# Configuration -# ============================================================================ +typeset -g PY_PYTHON="${PY_PYTHON:-python3}" +typeset -g PY_VENV="${PY_VENV:-venv}" +typeset -g PY_GIT_INIT="${PY_GIT_INIT:-true}" -typeset -g PY_TEMPLATE_BASE_DIR="${PY_TEMPLATE_BASE_DIR:-$HOME/projects}" -typeset -g PY_TEMPLATE_PYTHON="${PY_TEMPLATE_PYTHON:-python3}" -typeset -g PY_TEMPLATE_VENV_NAME="${PY_TEMPLATE_VENV_NAME:-venv}" -typeset -g PY_TEMPLATE_USE_POETRY="${PY_TEMPLATE_USE_POETRY:-false}" -typeset -g PY_TEMPLATE_GIT_INIT="${PY_TEMPLATE_GIT_INIT:-true}" - -# ============================================================================ -# Helper Functions -# ============================================================================ - -_py_print_step() { - echo -e "${DF_BLUE}==>${DF_NC} $1" -} - -_py_print_success() { - echo -e "${DF_GREEN}✓${DF_NC} $1" -} - -_py_print_info() { - echo -e "${DF_CYAN}ℹ${DF_NC} $1" -} - -_py_check_project_name() { - local name="$1" - if [[ -z "$name" ]]; then - echo -e "${DF_YELLOW}⚠${DF_NC} Project name required" - return 1 - fi - if [[ -d "$name" ]]; then - echo -e "${DF_YELLOW}⚠${DF_NC} Directory '$name' already exists" - return 1 - fi +_py_check_name() { + [[ -z "$1" ]] && { df_print_warning "Project name required"; return 1; } + [[ -d "$1" ]] && { df_print_warning "Directory '$1' exists"; return 1; } return 0 } -_py_create_venv() { - local project_dir="$1" - _py_print_step "Creating virtual environment" - - if [[ "$PY_TEMPLATE_USE_POETRY" == "true" ]] && command -v poetry &>/dev/null; then - cd "$project_dir" - poetry init --no-interaction - poetry env use "$PY_TEMPLATE_PYTHON" - _py_print_success "Poetry environment created" - else - "$PY_TEMPLATE_PYTHON" -m venv "$project_dir/$PY_TEMPLATE_VENV_NAME" - _py_print_success "Virtual environment created: $PY_TEMPLATE_VENV_NAME" - fi +_py_venv() { + df_print_step "Creating virtual environment" + "$PY_PYTHON" -m venv "$1/$PY_VENV" + df_print_success "Created: $PY_VENV" } -_py_create_gitignore() { - local project_dir="$1" - cat > "$project_dir/.gitignore" << 'EOF' +_py_gitignore() { + cat > "$1/.gitignore" << 'EOF' __pycache__/ *.py[cod] -*$py.class *.so -.Python build/ dist/ *.egg-info/ venv/ -env/ -.venv -.vscode/ -.idea/ -*.swp -.pytest_cache/ -.coverage -htmlcov/ +.venv/ .env *.log -*.db -*.sqlite3 +.pytest_cache/ .mypy_cache/ EOF - _py_print_success "Created .gitignore" + df_print_success "Created .gitignore" } -_py_init_git() { - local project_dir="$1" - if [[ "$PY_TEMPLATE_GIT_INIT" == "true" ]]; then - cd "$project_dir" - git init - git add . - git commit -m "Initial commit: project scaffolding" - _py_print_success "Git repository initialized" - fi +_py_git() { + [[ "$PY_GIT_INIT" == "true" ]] && { cd "$1"; git init; git add .; git commit -m "Initial commit"; df_print_success "Git initialized"; } } -_py_show_next_steps() { - local project_name="$1" - local has_venv="$2" - echo - echo -e "${DF_CYAN}Next steps:${DF_NC}" - echo " cd $project_name" - if [[ "$has_venv" == "true" ]]; then - if [[ "$PY_TEMPLATE_USE_POETRY" == "true" ]]; then - echo " poetry shell" - else - echo " source $PY_TEMPLATE_VENV_NAME/bin/activate" - fi - fi - echo " # Start coding!" - echo +_py_next() { + echo "" + df_print_section "Next steps" + df_print_indent "cd $1" + df_print_indent "source $PY_VENV/bin/activate" } -# ============================================================================ -# Base Python Project Template -# ============================================================================ - py-new() { - local project_name="$1" - _py_check_project_name "$project_name" || return 1 - - df_print_func_name "Python Project: $project_name" - - _py_print_step "Creating project structure" - mkdir -p "$project_name"/{src,tests,docs} - touch "$project_name/src/__init__.py" - touch "$project_name/tests/__init__.py" - - cat > "$project_name/src/main.py" << 'EOF' + _py_check_name "$1" || return 1 + df_print_func_name "Python Project: $1" + mkdir -p "$1"/{src,tests} + touch "$1/src/__init__.py" "$1/tests/__init__.py" + cat > "$1/src/main.py" << 'EOF' #!/usr/bin/env python3 -"""Main module.""" - def main(): - print("Hello from Python!") + print("Hello!") if __name__ == "__main__": main() EOF - - cat > "$project_name/requirements.txt" << 'EOF' -# Production dependencies -# Development: pytest, black, flake8, mypy -EOF - - _py_print_success "Project structure created" - _py_create_venv "$project_name" - _py_create_gitignore "$project_name" - _py_init_git "$project_name" - - echo - _py_print_success "Project '$project_name' created successfully!" - _py_show_next_steps "$project_name" "true" + echo "# Dependencies" > "$1/requirements.txt" + _py_venv "$1"; _py_gitignore "$1"; _py_git "$1" + df_print_success "Created: $1" + _py_next "$1" } -# ============================================================================ -# Django Project Template -# ============================================================================ - -py-django() { - local project_name="$1" - _py_check_project_name "$project_name" || return 1 - - df_print_func_name "Django Project: $project_name" - - mkdir -p "$project_name" - _py_create_venv "$project_name" - - _py_print_step "Installing Django" - cd "$project_name" - "$PY_TEMPLATE_VENV_NAME/bin/pip" install django - - _py_print_step "Creating Django project structure" - "$PY_TEMPLATE_VENV_NAME/bin/django-admin" startproject config . - - cat > "requirements.txt" << 'EOF' -Django>=4.2.0 -python-decouple>=3.8 -EOF - - mkdir -p apps static templates media - _py_create_gitignore "." - _py_init_git "." - cd .. - - echo - _py_print_success "Django project '$project_name' created!" - _py_show_next_steps "$project_name" "true" -} - -# ============================================================================ -# Flask Project Template -# ============================================================================ - py-flask() { - local project_name="$1" - _py_check_project_name "$project_name" || return 1 - - df_print_func_name "Flask Project: $project_name" - - mkdir -p "$project_name"/{app/{templates,static/{css,js}},tests} - _py_create_venv "$project_name" - - cd "$project_name" - _py_print_step "Installing Flask" - "$PY_TEMPLATE_VENV_NAME/bin/pip" install flask - - cat > "app/__init__.py" << 'EOF' + _py_check_name "$1" || return 1 + df_print_func_name "Flask Project: $1" + mkdir -p "$1"/{app/{templates,static},tests} + _py_venv "$1" + df_print_step "Installing Flask" + "$1/$PY_VENV/bin/pip" install flask -q + cat > "$1/app/__init__.py" << 'EOF' from flask import Flask - -def create_app(config=None): +def create_app(): app = Flask(__name__) - if config: - app.config.from_object(config) from app.routes import main app.register_blueprint(main) return app EOF - - cat > "app/routes.py" << 'EOF' + cat > "$1/app/routes.py" << 'EOF' from flask import Blueprint, render_template - main = Blueprint('main', __name__) - @main.route('/') def index(): return render_template('index.html') EOF - - cat > "app.py" << 'EOF' + echo '

Flask

' > "$1/app/templates/index.html" + cat > "$1/app.py" << 'EOF' #!/usr/bin/env python3 from app import create_app - app = create_app() - if __name__ == '__main__': - app.run(debug=True, host='0.0.0.0', port=5000) + app.run(debug=True) EOF - chmod +x app.py - - cat > "app/templates/index.html" << 'EOF' - -Flask App -

Welcome to Flask!

-EOF - - cat > "requirements.txt" << 'EOF' -Flask>=3.0.0 -python-decouple>=3.8 -EOF - - _py_create_gitignore "." - _py_init_git "." - cd .. - - echo - _py_print_success "Flask project '$project_name' created!" - _py_show_next_steps "$project_name" "true" + echo "Flask>=3.0.0" > "$1/requirements.txt" + _py_gitignore "$1"; _py_git "$1" + df_print_success "Created: $1" + _py_next "$1" } -# ============================================================================ -# FastAPI Project Template -# ============================================================================ - py-fastapi() { - local project_name="$1" - _py_check_project_name "$project_name" || return 1 - - df_print_func_name "FastAPI Project: $project_name" - - mkdir -p "$project_name"/{app/{api,models,schemas},tests} - _py_create_venv "$project_name" - - cd "$project_name" - _py_print_step "Installing FastAPI" - "$PY_TEMPLATE_VENV_NAME/bin/pip" install fastapi uvicorn[standard] pydantic - - touch "app/__init__.py" - - cat > "app/main.py" << 'EOF' + _py_check_name "$1" || return 1 + df_print_func_name "FastAPI Project: $1" + mkdir -p "$1"/{app,tests} + _py_venv "$1" + df_print_step "Installing FastAPI" + "$1/$PY_VENV/bin/pip" install fastapi uvicorn -q + cat > "$1/app/main.py" << 'EOF' from fastapi import FastAPI -from fastapi.middleware.cors import CORSMiddleware - -app = FastAPI(title="My API", version="0.1.0") - -app.add_middleware( - CORSMiddleware, - allow_origins=["*"], - allow_methods=["*"], - allow_headers=["*"], -) - +app = FastAPI() @app.get("/") def root(): - return {"message": "Welcome to FastAPI"} - + return {"message": "Hello"} @app.get("/health") def health(): - return {"status": "healthy"} + return {"status": "ok"} EOF - - cat > "run.py" << 'EOF' + cat > "$1/run.py" << 'EOF' #!/usr/bin/env python3 import uvicorn - if __name__ == "__main__": uvicorn.run("app.main:app", host="0.0.0.0", port=8000, reload=True) EOF - chmod +x run.py - - cat > "requirements.txt" << 'EOF' -fastapi>=0.104.0 -uvicorn[standard]>=0.24.0 -pydantic>=2.5.0 -EOF - - _py_create_gitignore "." - _py_init_git "." - cd .. - - echo - _py_print_success "FastAPI project '$project_name' created!" - _py_print_info "Docs at: http://localhost:8000/docs" - _py_show_next_steps "$project_name" "true" + echo -e "fastapi>=0.104.0\nuvicorn>=0.24.0" > "$1/requirements.txt" + _py_gitignore "$1"; _py_git "$1" + df_print_success "Created: $1" + df_print_info "Docs: http://localhost:8000/docs" + _py_next "$1" } -# ============================================================================ -# Data Science Project Template -# ============================================================================ - -py-data() { - local project_name="$1" - _py_check_project_name "$project_name" || return 1 - - df_print_func_name "Data Science Project: $project_name" - - mkdir -p "$project_name"/{data/{raw,processed},notebooks,src,models,reports/figures} - _py_create_venv "$project_name" - - cd "$project_name" - _py_print_step "Installing data science packages" - "$PY_TEMPLATE_VENV_NAME/bin/pip" install pandas numpy matplotlib seaborn jupyter - - touch "src/__init__.py" - touch data/raw/.gitkeep data/processed/.gitkeep - - cat > "requirements.txt" << 'EOF' -pandas>=2.1.0 -numpy>=1.24.0 -matplotlib>=3.8.0 -seaborn>=0.13.0 -jupyter>=1.0.0 -scikit-learn>=1.3.0 -EOF - - _py_create_gitignore "." - cat >> ".gitignore" << 'EOF' -*.pkl -*.h5 -*.parquet -data/raw/* -data/processed/* -!data/raw/.gitkeep -!data/processed/.gitkeep -models/*.pkl -.ipynb_checkpoints -EOF - - _py_init_git "." - cd .. - - echo - _py_print_success "Data science project '$project_name' created!" - _py_print_info "Start Jupyter: jupyter notebook" - _py_show_next_steps "$project_name" "true" -} - -# ============================================================================ -# CLI Tool Project Template -# ============================================================================ - py-cli() { - local project_name="$1" - _py_check_project_name "$project_name" || return 1 - - df_print_func_name "CLI Tool Project: $project_name" - - mkdir -p "$project_name"/{src/$project_name,tests} - _py_create_venv "$project_name" - - cd "$project_name" - _py_print_step "Installing click" - "$PY_TEMPLATE_VENV_NAME/bin/pip" install click - - cat > "src/$project_name/__init__.py" << 'EOF' -__version__ = "0.1.0" -EOF - - cat > "src/$project_name/cli.py" << 'EOF' + _py_check_name "$1" || return 1 + df_print_func_name "CLI Project: $1" + mkdir -p "$1"/{src/$1,tests} + _py_venv "$1" + df_print_step "Installing click" + "$1/$PY_VENV/bin/pip" install click -q + echo '__version__ = "0.1.0"' > "$1/src/$1/__init__.py" + cat > "$1/src/$1/cli.py" << 'EOF' #!/usr/bin/env python3 import click - @click.group() @click.version_option() def cli(): - """CLI tool - A command-line utility.""" pass - @cli.command() @click.argument('name', default='World') def greet(name): - """Greet someone.""" click.echo(f"Hello, {name}!") - if __name__ == '__main__': cli() EOF - - cat > "setup.py" << EOF -from setuptools import setup, find_packages - -setup( - name="$project_name", - version="0.1.0", - packages=find_packages(where="src"), - package_dir={"": "src"}, - install_requires=["click>=8.0.0"], - entry_points={"console_scripts": ["$project_name=$project_name.cli:cli"]}, -) -EOF - - cat > "requirements.txt" << 'EOF' -click>=8.1.0 -EOF - - _py_create_gitignore "." - _py_init_git "." - cd .. - - echo - _py_print_success "CLI tool project '$project_name' created!" - _py_print_info "Install with: pip install -e $project_name" - _py_show_next_steps "$project_name" "true" + echo "click>=8.1.0" > "$1/requirements.txt" + _py_gitignore "$1"; _py_git "$1" + df_print_success "Created: $1" + df_print_info "Install: pip install -e $1" + _py_next "$1" } -# ============================================================================ -# Aliases -# ============================================================================ - -alias pynew='py-new' -alias pydjango='py-django' -alias pyflask='py-flask' -alias pyfast='py-fastapi' -alias pydata='py-data' -alias pycli='py-cli' - -# Quick venv activation venv() { - if [[ -d "venv" ]]; then - source venv/bin/activate - elif [[ -d ".venv" ]]; then - source .venv/bin/activate - elif [[ -d "env" ]]; then - source env/bin/activate - else - echo "No virtual environment found (venv, .venv, or env)" - return 1 - fi + [[ -d "venv" ]] && source venv/bin/activate && return + [[ -d ".venv" ]] && source .venv/bin/activate && return + df_print_error "No venv found" } + +alias pynew='py-new' pyflask='py-flask' pyfast='py-fastapi' pycli='py-cli' diff --git a/zsh/functions/smart-suggest.zsh b/zsh/functions/smart-suggest.zsh index 70803db..f17685b 100644 --- a/zsh/functions/smart-suggest.zsh +++ b/zsh/functions/smart-suggest.zsh @@ -1,291 +1,82 @@ # ============================================================================ # Smart Command Suggestions for Zsh # ============================================================================ -# Provides intelligent suggestions when commands fail or could be improved -# -# Features: -# - Typo correction for common commands -# - Suggests existing aliases for frequently typed commands -# - "Did you mean?" for unknown commands -# - Package installation suggestions for missing commands -# ============================================================================ -# Source shared colors (with fallback) -source "${0:A:h}/../lib/colors.zsh" 2>/dev/null || \ -source "$HOME/.dotfiles/zsh/lib/colors.zsh" 2>/dev/null || { - typeset -g DF_CYAN=$'\033[0;36m' DF_YELLOW=$'\033[1;33m' - typeset -g DF_GREEN=$'\033[0;32m' DF_RED=$'\033[0;31m' - typeset -g DF_DIM=$'\033[2m' DF_NC=$'\033[0m' -} - -# ============================================================================ -# Configuration -# ============================================================================ +source "${0:A:h}/../lib/utils.zsh" 2>/dev/null || \ +source "$HOME/.dotfiles/zsh/lib/utils.zsh" 2>/dev/null typeset -g SMART_SUGGEST_ENABLED=true -typeset -g SMART_SUGGEST_TYPOS=true -typeset -g SMART_SUGGEST_ALIASES=true -typeset -g SMART_SUGGEST_PACKAGES=true -typeset -g SMART_SUGGEST_HISTORY=true typeset -g SMART_SUGGEST_TRACK_FILE="$HOME/.cache/smart-suggest-track" -# ============================================================================ -# Common Typo Database -# ============================================================================ - typeset -gA TYPO_CORRECTIONS=( - # Git typos - [gti]="git" [gitt]="git" [got]="git" [gut]="git" [gi]="git" - [giit]="git" [ggit]="git" [gitst]="git st" [gits]="git s" - [gitl]="git l" [gitd]="git d" [gitp]="git p" - [psuh]="push" [psull]="pull" [pul]="pull" [puhs]="push" - [stauts]="status" [statis]="status" [statuus]="status" - [comit]="commit" [commti]="commit" [commt]="commit" - [chekcout]="checkout" [chekout]="checkout" [checkou]="checkout" - [branhc]="branch" [barnch]="branch" [bracnh]="branch" - [marge]="merge" [merg]="merge" [stsh]="stash" [stahs]="stash" - - # Docker typos - [dokcer]="docker" [doker]="docker" [docekr]="docker" - [dcoker]="docker" [dockr]="docker" [docke]="docker" - [docker-compoes]="docker-compose" [docker-compsoe]="docker-compose" - [dokcer-compose]="docker-compose" - - # Common command typos - [sl]="ls" [l]="ls" [sls]="ls" [lss]="ls" - [cta]="cat" [catt]="cat" [caat]="cat" - [grpe]="grep" [gerp]="grep" [gre]="grep" [grepp]="grep" - [mkdri]="mkdir" [mkdr]="mkdir" [mdkir]="mkdir" [mdir]="mkdir" - [rn]="rm" [rmm]="rm" [chmdo]="chmod" [chomd]="chmod" - [chonw]="chown" [cown]="chown" [tarr]="tar" [tart]="tar" - [wegt]="wget" [wgte]="wget" [weget]="wget" - [crul]="curl" [curll]="curl" - [pytohn]="python" [pyhton]="python" [pythn]="python" - [pyton]="python" [pthon]="python" [pytho]="python" - [ndoe]="node" [noed]="node" [noode]="node" - [npn]="npm" [nmpm]="npm" [nppm]="npm" - [yran]="yarn" [yaarn]="yarn" [yanr]="yarn" - [suod]="sudo" [sudi]="sudo" [sduo]="sudo" [sudoo]="sudo" - [sssh]="ssh" [shh]="ssh" [scpp]="scp" [spcp]="scp" - [vmi]="vim" [imv]="vim" [viim]="vim" - [cde]="code" [cdoe]="code" [cod]="code" - [clera]="clear" [cler]="clear" [claer]="clear" - [ecoh]="echo" [ehco]="echo" [echoo]="echo" - [exti]="exit" [ext]="exit" [exitt]="exit" [eixt]="exit" - [histroy]="history" [hisotry]="history" [hsitory]="history" - [histrory]="history" [maek]="make" [mkae]="make" - [amke]="make" [makee]="make" [ccd]="cd" [cdd]="cd" + [gti]="git" [gitt]="git" [got]="git" [gi]="git" + [gitst]="git st" [gits]="git s" [gitp]="git p" + [psuh]="push" [psull]="pull" [pul]="pull" + [stauts]="status" [comit]="commit" [commti]="commit" + [chekcout]="checkout" [branhc]="branch" [marge]="merge" + [dokcer]="docker" [doker]="docker" [dcoker]="docker" + [sl]="ls" [sls]="ls" [cta]="cat" [grpe]="grep" [gerp]="grep" + [mkdri]="mkdir" [chmdo]="chmod" [suod]="sudo" [sduo]="sudo" + [pytohn]="python" [pyhton]="python" [ndoe]="node" + [vmi]="vim" [cde]="code" [clera]="clear" [exti]="exit" ) -# ============================================================================ -# Package Manager Detection -# ============================================================================ - -_ss_get_package_manager() { - if command -v apt-get &>/dev/null; then echo "apt" - elif command -v dnf &>/dev/null; then echo "dnf" - elif command -v pacman &>/dev/null; then echo "pacman" - elif command -v brew &>/dev/null; then echo "brew" - else echo "" - fi -} - typeset -gA COMMAND_PACKAGES=( - [htop]="htop" [tree]="tree" [jq]="jq" - [fd]="fd-find:apt fd:pacman fd:brew" [rg]="ripgrep" - [bat]="bat" [eza]="eza" [exa]="exa" [fzf]="fzf" - [tldr]="tldr" [ncdu]="ncdu" [duf]="duf" [dust]="dust" - [procs]="procs" [bottom]="bottom" [btm]="bottom" - [lazygit]="lazygit" [lazydocker]="lazydocker" - [neofetch]="neofetch" [fastfetch]="fastfetch" - [httpie]="httpie" [http]="httpie" - [delta]="git-delta:apt delta:pacman git-delta:brew" - [glow]="glow" [navi]="navi" + [htop]="htop" [tree]="tree" [jq]="jq" [fd]="fd" [rg]="ripgrep" + [bat]="bat" [eza]="eza" [fzf]="fzf" [tldr]="tldr" [ncdu]="ncdu" + [lazygit]="lazygit" [neofetch]="neofetch" [delta]="git-delta" ) -_ss_suggest_package() { - local cmd="$1" - local pm=$(_ss_get_package_manager) - - [[ -z "$pm" ]] && return 1 - - local pkg_info="${COMMAND_PACKAGES[$cmd]}" - [[ -z "$pkg_info" ]] && return 1 - - local pkg="" - - if [[ "$pkg_info" == *":"* ]]; then - for entry in ${(s: :)pkg_info}; do - local p="${entry%%:*}" - local m="${entry##*:}" - if [[ "$m" == "$pm" ]]; then - pkg="$p" - break - fi - done - [[ -z "$pkg" ]] && pkg="${${(s: :)pkg_info}[1]%%:*}" - else - pkg="$pkg_info" - fi - - [[ -z "$pkg" ]] && return 1 - - local install_cmd="" - case "$pm" in - apt) install_cmd="sudo apt install $pkg" ;; - dnf) install_cmd="sudo dnf install $pkg" ;; - pacman) install_cmd="sudo pacman -S $pkg" ;; - brew) install_cmd="brew install $pkg" ;; - esac - - echo "$install_cmd" -} - -# ============================================================================ -# Alias Tracking -# ============================================================================ - -_ss_track_command() { - [[ "$SMART_SUGGEST_ALIASES" != true ]] && return - +_ss_track() { local cmd="$1" [[ ${#cmd} -lt 8 ]] && return - - mkdir -p "$(dirname "$SMART_SUGGEST_TRACK_FILE")" + df_ensure_dir "$(dirname "$SMART_SUGGEST_TRACK_FILE")" echo "$cmd" >> "$SMART_SUGGEST_TRACK_FILE" - local count=$(grep -Fc "$cmd" "$SMART_SUGGEST_TRACK_FILE" 2>/dev/null || echo 0) - - if [[ $count -ge 10 && $((count % 10)) -eq 0 ]]; then - _ss_suggest_alias_for "$cmd" "$count" + if (( count >= 10 && count % 10 == 0 )); then + local existing=$(alias | grep -F "='$cmd'" | head -1 | cut -d= -f1) + [[ -n "$existing" ]] && df_print_info "You have alias: $existing" || \ + df_print_info "Consider: alias xyz='$cmd'" fi } -_ss_suggest_alias_for() { - local cmd="$1" - local count="$2" - - local existing=$(alias | grep -F "='$cmd'" | head -1 | cut -d= -f1) - - if [[ -n "$existing" ]]; then - echo - echo -e "${DF_CYAN}💡 Tip:${DF_NC} You've typed '${DF_YELLOW}$cmd${DF_NC}' $count times" - echo -e " You already have an alias: ${DF_GREEN}$existing${DF_NC}" - else - local suggested=$(echo "$cmd" | awk '{ - for(i=1; i<=NF && i<=3; i++) - printf substr($i,1,1) - }') - - echo - echo -e "${DF_CYAN}💡 Tip:${DF_NC} You've typed '${DF_YELLOW}$cmd${DF_NC}' $count times" - echo -e " Consider adding: ${DF_GREEN}alias $suggested='$cmd'${DF_NC}" - fi -} - -# ============================================================================ -# Command Not Found Handler -# ============================================================================ - command_not_found_handler() { - local cmd="$1" - shift - local args="$@" + local cmd="$1"; shift + [[ "$SMART_SUGGEST_ENABLED" != true ]] && { echo "zsh: command not found: $cmd"; return 127; } - [[ "$SMART_SUGGEST_ENABLED" != true ]] && { - echo "zsh: command not found: $cmd" - return 127 - } + df_print_error "Command not found: $cmd" - echo -e "${DF_RED}✗${DF_NC} Command not found: ${DF_YELLOW}$cmd${DF_NC}" + local correction="${TYPO_CORRECTIONS[$cmd]}" + [[ -n "$correction" ]] && { df_print_info "Did you mean: $correction?"; df_print_indent "Run: $correction $@"; } - local suggestion_made=false - - if [[ "$SMART_SUGGEST_TYPOS" == true ]]; then - local correction="${TYPO_CORRECTIONS[$cmd]}" - if [[ -n "$correction" ]]; then - echo -e "${DF_CYAN}→${DF_NC} Did you mean: ${DF_GREEN}$correction${DF_NC}?" - echo -e " ${DF_DIM}Run: $correction $args${DF_NC}" - suggestion_made=true - fi - fi - - if [[ "$suggestion_made" != true ]]; then - local similar=$(compgen -c 2>/dev/null | grep -i "^${cmd:0:3}" | head -3 | tr '\n' ', ' | sed 's/,$//') - if [[ -n "$similar" ]]; then - echo -e "${DF_CYAN}→${DF_NC} Similar commands: ${DF_GREEN}$similar${DF_NC}" - suggestion_made=true - fi - fi - - if [[ "$SMART_SUGGEST_PACKAGES" == true ]]; then - local install_cmd=$(_ss_suggest_package "$cmd") - if [[ -n "$install_cmd" ]]; then - echo -e "${DF_CYAN}→${DF_NC} To install: ${DF_GREEN}$install_cmd${DF_NC}" - suggestion_made=true - fi - fi + local pkg="${COMMAND_PACKAGES[$cmd]}" + [[ -n "$pkg" ]] && df_print_info "Install: sudo pacman -S $pkg" return 127 } -# ============================================================================ -# Hooks -# ============================================================================ - -_ss_preexec_hook() { - local cmd="$1" - local first_word="${cmd%% *}" - _ss_track_command "$cmd" -} - -_ss_precmd_hook() { - local exit_code=$? - [[ $exit_code -eq 0 ]] && return - - local last_cmd=$(fc -ln -1 2>/dev/null | sed 's/^[[:space:]]*//') - [[ -z "$last_cmd" ]] && return - - local first_word="${last_cmd%% *}" - - if [[ "$first_word" == "git" && $exit_code -ne 0 ]]; then - local git_subcmd=$(echo "$last_cmd" | awk '{print $2}') - local correction="${TYPO_CORRECTIONS[$git_subcmd]}" - - if [[ -n "$correction" ]]; then - echo -e "${DF_CYAN}→${DF_NC} Did you mean: ${DF_GREEN}git $correction${DF_NC}?" - fi - fi -} - -# ============================================================================ -# Quick Fix Function -# ============================================================================ - fuck() { - local last_cmd=$(fc -ln -1 2>/dev/null | sed 's/^[[:space:]]*//') - local first_word="${last_cmd%% *}" - - local correction="${TYPO_CORRECTIONS[$first_word]}" - - if [[ -n "$correction" ]]; then - local fixed_cmd="${last_cmd/$first_word/$correction}" - echo -e "${DF_GREEN}Running:${DF_NC} $fixed_cmd" - eval "$fixed_cmd" - else - echo "No automatic fix available" - echo "Last command: $last_cmd" - fi + local last=$(fc -ln -1 2>/dev/null | sed 's/^[[:space:]]*//') + local first="${last%% *}" + local fix="${TYPO_CORRECTIONS[$first]}" + [[ -n "$fix" ]] && { df_print_step "Running: ${last/$first/$fix}"; eval "${last/$first/$fix}"; } || df_print_warning "No fix for: $last" } -# ============================================================================ -# Setup Hooks -# ============================================================================ +_ss_preexec() { _ss_track "$1"; } +_ss_precmd() { + local exit=$?; (( exit == 0 )) && return + local last=$(fc -ln -1 2>/dev/null | sed 's/^[[:space:]]*//') + [[ "${last%% *}" == "git" ]] && { + local sub=$(echo "$last" | awk '{print $2}') + local fix="${TYPO_CORRECTIONS[$sub]}" + [[ -n "$fix" ]] && df_print_info "Did you mean: git $fix?" + } +} _ss_setup() { autoload -Uz add-zsh-hook - add-zsh-hook preexec _ss_preexec_hook - add-zsh-hook precmd _ss_precmd_hook + add-zsh-hook preexec _ss_preexec + add-zsh-hook precmd _ss_precmd } [[ "$SMART_SUGGEST_ENABLED" == true ]] && _ss_setup diff --git a/zsh/functions/snapper.zsh b/zsh/functions/snapper.zsh index 75d9237..65b464f 100644 --- a/zsh/functions/snapper.zsh +++ b/zsh/functions/snapper.zsh @@ -2,253 +2,97 @@ # Snapper Snapshot Functions for CachyOS/Arch with limine-snapper-sync # ============================================================================ -# Source shared colors (with fallback) -source "${0:A:h}/../lib/colors.zsh" 2>/dev/null || \ -source "$HOME/.dotfiles/zsh/lib/colors.zsh" 2>/dev/null || { - typeset -g DF_GREEN=$'\033[0;32m' DF_YELLOW=$'\033[1;33m' - typeset -g DF_RED=$'\033[0;31m' DF_BLUE=$'\033[0;34m' DF_NC=$'\033[0m' -} - -# ============================================================================ -# Main Snapshot Function with Limine Validation -# ============================================================================ +source "${0:A:h}/../lib/utils.zsh" 2>/dev/null || \ +source "$HOME/.dotfiles/zsh/lib/utils.zsh" 2>/dev/null snap-create() { - local description="$*" - local snap_config="root" - local limine_conf="/boot/limine.conf" + local desc="$*" + local limine="/boot/limine.conf" df_print_func_name "Snapper Snapshot Creation" - if [[ -z "$description" ]]; then - echo -e "${DF_YELLOW}⚠${DF_NC} No description provided" - echo -n "Enter snapshot description: " - read description - [[ -z "$description" ]] && { echo -e "${DF_RED}✗${DF_NC} Description required. Aborting."; return 1; } + if [[ -z "$desc" ]]; then + df_print_warning "No description" + echo -n "Description: "; read desc + [[ -z "$desc" ]] && { df_print_error "Required"; return 1; } fi - [[ ! -f "$limine_conf" ]] && { echo -e "${DF_RED}✗${DF_NC} Limine config not found: $limine_conf"; return 1; } + [[ ! -f "$limine" ]] && { df_print_error "Limine not found: $limine"; return 1; } - echo -e "${DF_BLUE}==>${DF_NC} Checking limine.conf state before snapshot" - local before_checksum=$(sudo md5sum "$limine_conf" | awk '{print $1}') - local before_entries=$(sudo grep -cP "^\\s*///\\d+\\s*│" "$limine_conf" || echo "0") + df_print_step "Checking limine.conf before snapshot" + local before=$(sudo grep -cP "^\\s*///\\d+\\s*│" "$limine" || echo "0") + df_print_success "Before: $before entries" - echo -e "${DF_GREEN}✓${DF_NC} Before: $before_entries snapshot entries" - echo -e "${DF_GREEN}✓${DF_NC} Before checksum: $before_checksum" + df_print_step "Creating snapshot: \"$desc\"" + local num=$(sudo snapper -c root create --description "$desc" --print-number) + [[ -z "$num" ]] && { df_print_error "Failed"; return 1; } + df_print_success "Created: #$num" - echo -e "\n${DF_BLUE}==>${DF_NC} Creating snapshot: \"$description\"" - - local snapshot_num=$(sudo snapper -c "$snap_config" create --description "$description" --print-number) - - [[ -z "$snapshot_num" ]] && { echo -e "${DF_RED}✗${DF_NC} Failed to create snapshot"; return 1; } - - echo -e "${DF_GREEN}✓${DF_NC} Snapshot created: #$snapshot_num" - - echo -e "\n${DF_BLUE}==>${DF_NC} Triggering limine-snapper-sync service..." - - if sudo systemctl start limine-snapper-sync.service; then - echo -e "${DF_GREEN}✓${DF_NC} Service triggered successfully" - else - echo -e "${DF_YELLOW}⚠${DF_NC} Failed to trigger service (may run automatically)" - fi - - echo -e "${DF_BLUE}==>${DF_NC} Waiting for limine-snapper-sync to update limine.conf..." + df_print_step "Triggering limine-snapper-sync..." + sudo systemctl start limine-snapper-sync.service && df_print_success "Triggered" || df_print_warning "May run automatically" sleep 2 - echo -e "${DF_BLUE}==>${DF_NC} Validating limine.conf update" - local after_checksum=$(sudo md5sum "$limine_conf" | awk '{print $1}') - local after_entries=$(sudo grep -cP "^\\s*///\\d+\\s*│" "$limine_conf" || echo "0") + df_print_step "Validating" + local after=$(sudo grep -cP "^\\s*///\\d+\\s*│" "$limine" || echo "0") - local validation_passed=true - - if [[ "$before_checksum" == "$after_checksum" ]]; then - echo -e "${DF_RED}✗${DF_NC} limine.conf was NOT updated (checksum unchanged)" - validation_passed=false + if sudo grep -qP "^\\s*///$num\\s*│" "$limine"; then + df_print_success "Snapshot #$num in limine.conf" + (( after > before )) && df_print_success "Added $((after - before)) entry" else - echo -e "${DF_GREEN}✓${DF_NC} limine.conf was updated" - fi - - if [[ "$after_entries" -le "$before_entries" ]]; then - echo -e "${DF_RED}✗${DF_NC} No new snapshot entry added to limine.conf" - validation_passed=false - else - local new_entries=$((after_entries - before_entries)) - echo -e "${DF_GREEN}✓${DF_NC} Added $new_entries new snapshot entry/entries" - fi - - echo -e "\n${DF_BLUE}==>${DF_NC} Searching for snapshot #$snapshot_num in limine.conf" - - if sudo grep -qP "^\\s*///$snapshot_num\\s*│" "$limine_conf"; then - echo -e "${DF_GREEN}✓${DF_NC} Found snapshot #$snapshot_num in limine.conf" - echo -e "\n${DF_BLUE}Snapshot entry:${DF_NC}" - local entry_line=$(sudo grep -nP "^\\s*///$snapshot_num\\s*│" "$limine_conf" | head -n 1 | cut -d: -f1) - [[ -n "$entry_line" ]] && sudo sed -n "${entry_line}p; $((entry_line+1))p" "$limine_conf" | sed 's/^/ /' - else - echo -e "${DF_RED}✗${DF_NC} Snapshot #$snapshot_num NOT found in limine.conf" - validation_passed=false + df_print_error "Snapshot #$num NOT in limine.conf" + return 1 fi echo "" - echo -e "${DF_CYAN}Summary:${DF_NC}" - echo -e " Snapshot Number: #$snapshot_num" - echo -e " Description: \"$description\"" - echo -e " Config: $snap_config" - echo -e " Before entries: $before_entries" - echo -e " After entries: $after_entries" - - if [[ "$validation_passed" == true ]]; then - echo -e " Status: ${DF_GREEN}✓ VALIDATED${DF_NC}" - echo -e "\n${DF_GREEN}✓${DF_NC} Snapshot created and limine.conf successfully updated!" - return 0 - else - echo -e " Status: ${DF_RED}✗ VALIDATION FAILED${DF_NC}" - echo -e "\n${DF_RED}✗${DF_NC} Snapshot created but limine.conf validation failed!" - echo -e "${DF_YELLOW}⚠${DF_NC} Check if limine-snapper-sync service is running properly" - echo -e "${DF_YELLOW}Run:${DF_NC} sudo systemctl status limine-snapper-sync.service" - return 1 - fi + df_print_section "Summary" + df_print_indent "Number: #$num" + df_print_indent "Description: $desc" } -# ============================================================================ -# Helper Functions -# ============================================================================ - snap-list() { local count="${1:-10}" - df_print_func_name "Snapper Snapshots (last $count)" - sudo snapper -c root list | tail -n "$((count + 1))" } snap-show() { - [[ -z "$1" ]] && { echo -e "${DF_RED}✗${DF_NC} Usage: snap-show "; return 1; } - - df_print_func_name "Snapshot #$1 Details" - + [[ -z "$1" ]] && { echo "Usage: snap-show "; return 1; } + df_print_func_name "Snapshot #$1" sudo snapper -c root list | grep "^\s*$1\s" - - echo -e "\n${DF_CYAN}In limine.conf:${DF_NC}" - if sudo grep -qP "^\\s*///$1\\s*│" /boot/limine.conf; then - local entry_line=$(sudo grep -nP "^\\s*///$1\\s*│" /boot/limine.conf | head -n 1 | cut -d: -f1) - [[ -n "$entry_line" ]] && sudo sed -n "${entry_line}p; $((entry_line+1))p" /boot/limine.conf - else - echo -e "${DF_YELLOW}⚠${DF_NC} Not found in limine.conf" - fi + echo "" + df_print_section "In limine.conf" + sudo grep -qP "^\\s*///$1\\s*│" /boot/limine.conf && \ + sudo grep -P "^\\s*///$1\\s*│" /boot/limine.conf || df_print_warning "Not found" } snap-delete() { - [[ -z "$1" ]] && { echo -e "${DF_RED}✗${DF_NC} Usage: snap-delete "; return 1; } + [[ -z "$1" ]] && { echo "Usage: snap-delete "; return 1; } + df_print_func_name "Delete Snapshot #$1" - local snapshot_num="$1" - local limine_conf="/boot/limine.conf" + local before=$(sudo grep -cP "^\\s*///\\d+\\s*│" /boot/limine.conf || echo "0") + sudo snapper -c root delete "$1" && df_print_success "Deleted #$1" || { df_print_error "Failed"; return 1; } - df_print_func_name "Delete Snapshot #$snapshot_num" + df_print_step "Syncing limine..." + sudo systemctl start limine-snapper-sync.service; sleep 2 - local before_entries=$(sudo grep -cP "^\\s*///\\d+\\s*│" "$limine_conf" || echo "0") - - sudo snapper -c root delete "$snapshot_num" - - if [[ $? -eq 0 ]]; then - echo -e "${DF_GREEN}✓${DF_NC} Snapshot #$snapshot_num deleted" - - echo -e "${DF_BLUE}==>${DF_NC} Triggering limine-snapper-sync..." - sudo systemctl start limine-snapper-sync.service - sleep 2 - - local after_entries=$(sudo grep -cP "^\\s*///\\d+\\s*│" "$limine_conf" || echo "0") - - if [[ "$after_entries" -lt "$before_entries" ]]; then - echo -e "${DF_GREEN}✓${DF_NC} limine.conf updated (removed entry)" - else - echo -e "${DF_YELLOW}⚠${DF_NC} limine.conf may not have been updated" - fi - - if ! sudo grep -qP "^\\s*///$snapshot_num\\s*│" "$limine_conf"; then - echo -e "${DF_GREEN}✓${DF_NC} Snapshot #$snapshot_num removed from limine.conf" - else - echo -e "${DF_RED}✗${DF_NC} Snapshot #$snapshot_num still in limine.conf!" - fi - else - echo -e "${DF_RED}✗${DF_NC} Failed to delete snapshot #$snapshot_num" - return 1 - fi -} - -snap-check-limine() { - local limine_conf="/boot/limine.conf" - - df_print_func_name "Limine Snapshot Entries" - - [[ ! -f "$limine_conf" ]] && { echo -e "${DF_RED}✗${DF_NC} Limine config not found: $limine_conf"; return 1; } - - local latest_snapshot=$(sudo snapper -c root list | tail -n +3 | grep -v "^\s*0\s" | tail -n 1 | awk '{print $1}') - [[ -z "$latest_snapshot" ]] && { echo -e "${DF_YELLOW}⚠${DF_NC} No snapshots found in snapper"; return 1; } - - echo -e "${DF_CYAN}Latest snapshot:${DF_NC} #$latest_snapshot" - - echo -e "${DF_BLUE}==>${DF_NC} Checking if latest snapshot is in limine.conf" - - if sudo grep -qP "^\\s*///$latest_snapshot\s*│" "$limine_conf"; then - echo -e "${DF_GREEN}✓${DF_NC} Latest snapshot #$latest_snapshot is present in limine.conf" - else - echo -e "${DF_RED}✗${DF_NC} Latest snapshot #$latest_snapshot is NOT in limine.conf" - fi - - echo -e "\n${DF_BLUE}==>${DF_NC} Counting snapshot entries" - local entry_count=$(sudo grep -cP "^\\s*///\\d+\\s*│" "$limine_conf" || echo "0") - echo -e "${DF_CYAN}Total snapshot entries:${DF_NC} $entry_count" + sudo grep -qP "^\\s*///$1\\s*│" /boot/limine.conf && df_print_error "Still in limine!" || df_print_success "Removed from limine" } snap-sync() { df_print_func_name "Limine-Snapper-Sync" - - echo -e "${DF_BLUE}==>${DF_NC} Manually triggering limine-snapper-sync..." - - if sudo systemctl start limine-snapper-sync.service; then - echo -e "${DF_GREEN}✓${DF_NC} Service triggered successfully" - sleep 2 - echo -e "\n${DF_CYAN}Service status:${DF_NC}" - sudo systemctl status limine-snapper-sync.service --no-pager -l | tail -n 10 - else - echo -e "${DF_RED}✗${DF_NC} Failed to trigger service" - return 1 - fi + df_print_step "Triggering sync..." + sudo systemctl start limine-snapper-sync.service && { sleep 2; df_print_success "Done"; } || df_print_error "Failed" } -snap-validate-service() { - df_print_func_name "Limine-Snapper-Sync Service Validation" - - echo -e "${DF_BLUE}==>${DF_NC} Checking service unit" - - if systemctl list-unit-files | grep -q "limine-snapper-sync.service"; then - echo -e "${DF_GREEN}✓${DF_NC} limine-snapper-sync.service unit exists" - else - echo -e "${DF_RED}✗${DF_NC} limine-snapper-sync.service unit NOT found" - echo -e "\n${DF_YELLOW}Install with:${DF_NC} paru -S limine-snapper-sync" - return 1 - fi - - echo -e "\n${DF_BLUE}==>${DF_NC} Checking if service is enabled" - - if systemctl is-enabled limine-snapper-sync.service &>/dev/null; then - echo -e "${DF_GREEN}✓${DF_NC} Service is enabled" - else - echo -e "${DF_YELLOW}⚠${DF_NC} Service is NOT enabled" - echo -e "${DF_YELLOW}Enable with:${DF_NC} sudo systemctl enable limine-snapper-sync.service" - fi - - echo -e "\n${DF_BLUE}==>${DF_NC} Recent service logs (last 10 lines)" - echo "" - sudo journalctl -u limine-snapper-sync.service -n 10 --no-pager | sed 's/^/ /' - - echo -e "\n${DF_GREEN}✓${DF_NC} Validation complete" +snap-check() { + df_print_func_name "Limine Snapshot Entries" + local latest=$(sudo snapper -c root list | tail -n +3 | grep -v "^\s*0\s" | tail -1 | awk '{print $1}') + [[ -z "$latest" ]] && { df_print_warning "No snapshots"; return 1; } + df_print_info "Latest: #$latest" + sudo grep -qP "^\\s*///$latest\\s*│" /boot/limine.conf && \ + df_print_success "Latest in limine.conf" || df_print_error "Latest NOT in limine.conf" + local count=$(sudo grep -cP "^\\s*///\\d+\\s*│" /boot/limine.conf || echo "0") + df_print_info "Total entries: $count" } -# Quick snapshot aliases -alias snap='snap-create' -alias snapls='snap-list' -alias snaprm='snap-delete' -alias snapshow='snap-show' -alias snapcheck='snap-check-limine' -alias snapsync='snap-sync' +alias snap='snap-create' snapls='snap-list' snaprm='snap-delete' snapcheck='snap-check' diff --git a/zsh/functions/ssh-manager.zsh b/zsh/functions/ssh-manager.zsh index 4c878d4..7b0cb5c 100644 --- a/zsh/functions/ssh-manager.zsh +++ b/zsh/functions/ssh-manager.zsh @@ -1,237 +1,96 @@ # ============================================================================ # SSH Session Manager with Tmux Integration # ============================================================================ -# Manage SSH connections with automatic tmux session handling -# ============================================================================ -# Source shared colors (with fallback) -source "${0:A:h}/../lib/colors.zsh" 2>/dev/null || \ -source "$HOME/.dotfiles/zsh/lib/colors.zsh" 2>/dev/null || { - typeset -g DF_GREEN=$'\033[0;32m' DF_BLUE=$'\033[0;34m' - typeset -g DF_YELLOW=$'\033[1;33m' DF_CYAN=$'\033[0;36m' - typeset -g DF_RED=$'\033[0;31m' DF_NC=$'\033[0m' -} - -# ============================================================================ -# Configuration -# ============================================================================ +source "${0:A:h}/../lib/utils.zsh" 2>/dev/null || \ +source "$HOME/.dotfiles/zsh/lib/utils.zsh" 2>/dev/null typeset -g SSH_PROFILES_FILE="${SSH_PROFILES_FILE:-$HOME/.dotfiles/.ssh-profiles}" typeset -g SSH_AUTO_TMUX="${SSH_AUTO_TMUX:-true}" -typeset -g SSH_TMUX_SESSION_PREFIX="${SSH_TMUX_SESSION_PREFIX:-ssh}" -typeset -g SSH_SYNC_DOTFILES="${SSH_SYNC_DOTFILES:-ask}" +typeset -g SSH_TMUX_PREFIX="${SSH_TMUX_PREFIX:-ssh}" -# ============================================================================ -# Helper Functions -# ============================================================================ - -_ssh_print_step() { echo -e "${DF_BLUE}==>${DF_NC} $1"; } -_ssh_print_success() { echo -e "${DF_GREEN}✓${DF_NC} $1"; } -_ssh_print_error() { echo -e "${DF_RED}✗${DF_NC} $1"; } -_ssh_print_info() { echo -e "${DF_CYAN}ℹ${DF_NC} $1"; } - -_ssh_init_profiles() { - if [[ ! -f "$SSH_PROFILES_FILE" ]]; then - mkdir -p "$(dirname "$SSH_PROFILES_FILE")" - cat > "$SSH_PROFILES_FILE" << 'EOF' -# SSH Connection Profiles -# Format: name|user@host|port|key_file|options|description -EOF - _ssh_print_success "Created SSH profiles file: $SSH_PROFILES_FILE" - fi +_ssh_init() { + df_ensure_file "$SSH_PROFILES_FILE" "# SSH Profiles: name|user@host|port|key|options|description" } -_ssh_parse_profile() { - local name="$1" - local line=$(grep "^${name}|" "$SSH_PROFILES_FILE" 2>/dev/null | head -1) +_ssh_parse() { + local line=$(grep "^${1}|" "$SSH_PROFILES_FILE" 2>/dev/null | head -1) [[ -z "$line" ]] && return 1 - IFS='|' read -r profile_name connection port key_file ssh_opts description <<< "$line" - echo "$connection|$port|$key_file|$ssh_opts|$description" + echo "$line" | cut -d'|' -f2- } -# ============================================================================ -# SSH Profile Management -# ============================================================================ - ssh-save() { - local name="$1" connection="$2" port="${3:-22}" key_file="${4:-}" options="${5:-}" description="${6:-}" - - _ssh_init_profiles - - [[ -z "$name" || -z "$connection" ]] && { - echo "Usage: ssh-save [port] [key_file] [options] [description]" - return 1 - } - - if grep -q "^${name}|" "$SSH_PROFILES_FILE" 2>/dev/null; then - echo -e "${DF_YELLOW}⚠${DF_NC} Profile '$name' already exists" - read -q "REPLY?Overwrite? [y/N]: "; echo - [[ ! "$REPLY" =~ ^[Yy]$ ]] && return 1 + local name="$1" conn="$2" port="${3:-22}" key="${4:-}" opts="${5:-}" desc="${6:-}" + [[ -z "$name" || -z "$conn" ]] && { echo "Usage: ssh-save [port] [key] [opts] [desc]"; return 1; } + _ssh_init + grep -q "^${name}|" "$SSH_PROFILES_FILE" 2>/dev/null && { + df_confirm "Overwrite '$name'?" || return 1 grep -v "^${name}|" "$SSH_PROFILES_FILE" > "${SSH_PROFILES_FILE}.tmp" mv "${SSH_PROFILES_FILE}.tmp" "$SSH_PROFILES_FILE" - fi - - echo "${name}|${connection}|${port}|${key_file}|${options}|${description}" >> "$SSH_PROFILES_FILE" - - _ssh_print_success "Saved SSH profile: $name" - echo " Connection: $connection" - [[ "$port" != "22" ]] && echo " Port: $port" - [[ -n "$key_file" ]] && echo " Key: $key_file" + } + echo "${name}|${conn}|${port}|${key}|${opts}|${desc}" >> "$SSH_PROFILES_FILE" + df_print_success "Saved: $name → $conn" } ssh-list() { - _ssh_init_profiles - - df_print_func_name "SSH Connection Profiles" - - local has_profiles=false - while IFS='|' read -r name connection port key options description; do - [[ "$name" =~ ^# ]] && continue - [[ -z "$name" ]] && continue - has_profiles=true - - echo -e "${DF_GREEN}●${DF_NC} ${DF_CYAN}$name${DF_NC}" - echo " Connection: $connection" - [[ "$port" != "22" && -n "$port" ]] && echo " Port: $port" - [[ -n "$key" ]] && echo " Key: $key" - [[ -n "$description" ]] && echo " Description: $description" - echo + _ssh_init + df_print_func_name "SSH Profiles" + local found=false + while IFS='|' read -r name conn port key opts desc; do + [[ "$name" =~ ^# || -z "$name" ]] && continue + found=true + df_print_indent "● $name → $conn" + [[ "$port" != "22" && -n "$port" ]] && df_print_indent " Port: $port" + [[ -n "$desc" ]] && df_print_indent " $desc" done < "$SSH_PROFILES_FILE" - - [[ "$has_profiles" != true ]] && { - _ssh_print_info "No profiles saved yet" - echo "Create a profile with: ssh-save myserver user@example.com" - } + [[ "$found" != true ]] && df_print_info "No profiles. Use: ssh-save name user@host" } ssh-delete() { - local name="$1" - [[ -z "$name" ]] && { echo "Usage: ssh-delete "; return 1; } - - _ssh_init_profiles - - if ! grep -q "^${name}|" "$SSH_PROFILES_FILE" 2>/dev/null; then - _ssh_print_error "Profile '$name' not found" - return 1 - fi - - grep -v "^${name}|" "$SSH_PROFILES_FILE" > "${SSH_PROFILES_FILE}.tmp" + [[ -z "$1" ]] && { echo "Usage: ssh-delete "; return 1; } + grep -q "^${1}|" "$SSH_PROFILES_FILE" 2>/dev/null || { df_print_error "Not found: $1"; return 1; } + grep -v "^${1}|" "$SSH_PROFILES_FILE" > "${SSH_PROFILES_FILE}.tmp" mv "${SSH_PROFILES_FILE}.tmp" "$SSH_PROFILES_FILE" - _ssh_print_success "Deleted profile: $name" + df_print_success "Deleted: $1" } ssh-connect() { - local name="$1" - local session_name="${2:-${SSH_TMUX_SESSION_PREFIX}-${name}}" + local name="$1" session="${2:-${SSH_TMUX_PREFIX}-${1}}" + [[ -z "$name" ]] && { ssh-list; return 1; } + _ssh_init + local data=$(_ssh_parse "$name") + [[ -z "$data" ]] && { df_print_error "Not found: $name"; return 1; } - [[ -z "$name" ]] && { echo "Usage: ssh-connect "; ssh-list; return 1; } + IFS='|' read -r conn port key opts desc <<< "$data" + df_print_step "Connecting: $name" + [[ -n "$desc" ]] && df_print_indent "$desc" - _ssh_init_profiles - - local profile_data=$(_ssh_parse_profile "$name") - [[ -z "$profile_data" ]] && { _ssh_print_error "Profile '$name' not found"; return 1; } - - IFS='|' read -r connection port key_file ssh_opts description <<< "$profile_data" - - _ssh_print_step "Connecting to: $name" - [[ -n "$description" ]] && echo " $description" - - local ssh_cmd="ssh" - [[ -n "$port" && "$port" != "22" ]] && ssh_cmd="$ssh_cmd -p $port" - [[ -n "$key_file" ]] && ssh_cmd="$ssh_cmd -i $key_file" - [[ -n "$ssh_opts" ]] && ssh_cmd="$ssh_cmd $ssh_opts" - ssh_cmd="$ssh_cmd $connection" + local cmd="ssh" + [[ -n "$port" && "$port" != "22" ]] && cmd="$cmd -p $port" + [[ -n "$key" ]] && cmd="$cmd -i $key" + [[ -n "$opts" ]] && cmd="$cmd $opts" + cmd="$cmd $conn" if [[ "$SSH_AUTO_TMUX" == "true" ]]; then - _ssh_print_info "Attaching to tmux session: $session_name" - local tmux_cmd="tmux attach-session -t $session_name 2>/dev/null || tmux new-session -s $session_name" - eval "$ssh_cmd -t '$tmux_cmd'" + df_print_info "Tmux session: $session" + eval "$cmd -t 'tmux attach -t $session 2>/dev/null || tmux new -s $session'" else - eval "$ssh_cmd" + eval "$cmd" fi } sshf() { - if ! command -v fzf &>/dev/null; then - _ssh_print_error "fzf not installed" - return 1 - fi - - _ssh_init_profiles - + df_require_cmd fzf || return 1 + _ssh_init local profiles=() - while IFS='|' read -r name connection port key options description; do - [[ "$name" =~ ^# ]] && continue - [[ -z "$name" ]] && continue - local display="$name → $connection" - [[ -n "$description" ]] && display="$display ($description)" - profiles+=("$name|$display") + while IFS='|' read -r name conn port key opts desc; do + [[ "$name" =~ ^# || -z "$name" ]] && continue + profiles+=("$name|$name → $conn") done < "$SSH_PROFILES_FILE" - - [[ ${#profiles[@]} -eq 0 ]] && { _ssh_print_info "No profiles saved"; return 1; } - - local selection=$(printf '%s\n' "${profiles[@]}" | \ - fzf --height=50% --layout=reverse --border=rounded --prompt='SSH > ' \ - --delimiter='|' --with-nth=2) - - [[ -n "$selection" ]] && ssh-connect "${selection%%|*}" + [[ ${#profiles[@]} -eq 0 ]] && { df_print_info "No profiles"; return 1; } + local sel=$(printf '%s\n' "${profiles[@]}" | fzf $(df_fzf_opts) --delimiter='|' --with-nth=2 --prompt='SSH > ') + [[ -n "$sel" ]] && ssh-connect "${sel%%|*}" } -ssh-reconnect() { - local name="${1:-last}" - - if [[ "$name" == "last" ]]; then - local last_profile=$(grep "ssh-connect" "$HISTFILE" 2>/dev/null | tail -1 | awk '{print $2}') - [[ -z "$last_profile" ]] && { _ssh_print_error "No previous connection found"; return 1; } - name="$last_profile" - fi - - _ssh_print_info "Reconnecting to: $name" - ssh-connect "$name" -} - -ssh-sync-dotfiles() { - local name="$1" - [[ -z "$name" ]] && { echo "Usage: ssh-sync-dotfiles "; return 1; } - - local profile_data=$(_ssh_parse_profile "$name") - [[ -z "$profile_data" ]] && { _ssh_print_error "Profile '$name' not found"; return 1; } - - IFS='|' read -r connection port key_file ssh_opts description <<< "$profile_data" - - local dotfiles_dir="${DOTFILES_DIR:-$HOME/.dotfiles}" - [[ ! -d "$dotfiles_dir" ]] && { _ssh_print_error "Dotfiles directory not found"; return 1; } - - df_print_func_name "Sync Dotfiles to: $connection" - - local rsync_cmd="rsync -avz --exclude='.git' --exclude='*.local'" - [[ -n "$port" && "$port" != "22" ]] && rsync_cmd="$rsync_cmd -e 'ssh -p $port'" - [[ -n "$key_file" ]] && rsync_cmd="$rsync_cmd -e 'ssh -i $key_file'" - rsync_cmd="$rsync_cmd $dotfiles_dir/ $connection:.dotfiles/" - - _ssh_print_info "Running: $rsync_cmd" - - if eval "$rsync_cmd"; then - _ssh_print_success "Dotfiles synced successfully" - else - _ssh_print_error "Failed to sync dotfiles" - return 1 - fi -} - -# ============================================================================ -# Aliases -# ============================================================================ - -alias sshl='ssh-list' -alias sshs='ssh-save' -alias sshc='ssh-connect' -alias sshd='ssh-delete' -alias sshr='ssh-reconnect' -alias sshsync='ssh-sync-dotfiles' - -# ============================================================================ -# Initialization -# ============================================================================ - -_ssh_init_profiles +alias sshl='ssh-list' sshs='ssh-save' sshc='ssh-connect' sshd='ssh-delete' +_ssh_init diff --git a/zsh/functions/systemd-helpers.zsh b/zsh/functions/systemd-helpers.zsh index a98064b..2b7eba2 100644 --- a/zsh/functions/systemd-helpers.zsh +++ b/zsh/functions/systemd-helpers.zsh @@ -1,335 +1,124 @@ # ============================================================================ # Systemd Integration for Arch/CachyOS # ============================================================================ -# Quick shortcuts and helpers for systemd service management -# -# Commands: -# sc - sudo systemctl -# scu - systemctl --user -# scr - restart and show status -# sce - enable and start -# scd - disable and stop -# sclog - follow journal logs -# sc-failed - show failed services -# sc-timers - show active timers -# sc-recent - recently started services -# sc-boot - boot time analysis -# ============================================================================ -# Source shared colors (with fallback) -source "${0:A:h}/../lib/colors.zsh" 2>/dev/null || \ -source "$HOME/.dotfiles/zsh/lib/colors.zsh" 2>/dev/null || { - typeset -g DF_GREEN=$'\033[0;32m' DF_YELLOW=$'\033[1;33m' - typeset -g DF_RED=$'\033[0;31m' DF_BLUE=$'\033[0;34m' - typeset -g DF_CYAN=$'\033[0;36m' DF_NC=$'\033[0m' -} +source "${0:A:h}/../lib/utils.zsh" 2>/dev/null || \ +source "$HOME/.dotfiles/zsh/lib/utils.zsh" 2>/dev/null -# ============================================================================ -# Core Systemctl Shortcuts -# ============================================================================ +# Core shortcuts +sc() { sudo systemctl "$@"; } +scu() { systemctl --user "$@"; } -# System-level systemctl (with sudo) -sc() { - sudo systemctl "$@" -} - -# User-level systemctl -scu() { - systemctl --user "$@" -} - -# Restart service and show status scr() { - local service="$1" - [[ -z "$service" ]] && { echo "Usage: scr "; return 1; } - - echo -e "${DF_BLUE}==>${DF_NC} Restarting ${service}..." - if sudo systemctl restart "$service"; then - echo -e "${DF_GREEN}✓${DF_NC} Restarted successfully" - echo "" - sudo systemctl status "$service" --no-pager -l - else - echo -e "${DF_RED}✗${DF_NC} Failed to restart ${service}" - return 1 - fi + [[ -z "$1" ]] && { echo "Usage: scr "; return 1; } + df_print_step "Restarting $1..." + sudo systemctl restart "$1" && { df_print_success "Restarted"; sudo systemctl status "$1" --no-pager -l; } || df_print_error "Failed" } -# Enable and start service sce() { - local service="$1" - [[ -z "$service" ]] && { echo "Usage: sce "; return 1; } - - echo -e "${DF_BLUE}==>${DF_NC} Enabling and starting ${service}..." - if sudo systemctl enable --now "$service"; then - echo -e "${DF_GREEN}✓${DF_NC} ${service} enabled and started" - sudo systemctl status "$service" --no-pager -l | head -15 - else - echo -e "${DF_RED}✗${DF_NC} Failed to enable ${service}" - return 1 - fi + [[ -z "$1" ]] && { echo "Usage: sce "; return 1; } + df_print_step "Enabling $1..." + sudo systemctl enable --now "$1" && { df_print_success "Enabled"; sudo systemctl status "$1" --no-pager -l | head -15; } || df_print_error "Failed" } -# Disable and stop service scd() { - local service="$1" - [[ -z "$service" ]] && { echo "Usage: scd "; return 1; } - - echo -e "${DF_BLUE}==>${DF_NC} Disabling and stopping ${service}..." - if sudo systemctl disable --now "$service"; then - echo -e "${DF_GREEN}✓${DF_NC} ${service} disabled and stopped" - else - echo -e "${DF_RED}✗${DF_NC} Failed to disable ${service}" - return 1 - fi + [[ -z "$1" ]] && { echo "Usage: scd "; return 1; } + df_print_step "Disabling $1..." + sudo systemctl disable --now "$1" && df_print_success "Disabled" || df_print_error "Failed" } -# Follow journal logs for a service sclog() { - local service="$1" - local lines="${2:-50}" - [[ -z "$service" ]] && { echo "Usage: sclog [lines]"; return 1; } - - echo -e "${DF_BLUE}==>${DF_NC} Following logs for ${service} (Ctrl+C to exit)..." - sudo journalctl -xeu "$service" -f -n "$lines" + [[ -z "$1" ]] && { echo "Usage: sclog "; return 1; } + df_print_step "Following logs for $1 (Ctrl+C to exit)..." + sudo journalctl -xeu "$1" -f -n "${2:-50}" } -# Show recent logs for a service (without follow) sclogs() { - local service="$1" - local lines="${2:-50}" - [[ -z "$service" ]] && { echo "Usage: sclogs [lines]"; return 1; } - - sudo journalctl -xeu "$service" -n "$lines" --no-pager + [[ -z "$1" ]] && { echo "Usage: sclogs "; return 1; } + sudo journalctl -xeu "$1" -n "${2:-50}" --no-pager } -# ============================================================================ -# Service Status Commands -# ============================================================================ - -# Show failed services (system and user) sc-failed() { df_print_func_name "Failed Services" - - echo -e "${DF_CYAN}System Services:${DF_NC}" - local sys_failed=$(systemctl --failed --no-pager --no-legend 2>/dev/null) - if [[ -z "$sys_failed" ]]; then - echo -e " ${DF_GREEN}✓${DF_NC} No failed system services" - else - echo "$sys_failed" | sed 's/^/ /' - fi - - echo -e "\n${DF_CYAN}User Services:${DF_NC}" - local user_failed=$(systemctl --user --failed --no-pager --no-legend 2>/dev/null) - if [[ -z "$user_failed" ]]; then - echo -e " ${DF_GREEN}✓${DF_NC} No failed user services" - else - echo "$user_failed" | sed 's/^/ /' - fi - + df_print_section "System" + local sys=$(systemctl --failed --no-pager --no-legend 2>/dev/null) + [[ -z "$sys" ]] && df_print_indent "✓ None" || echo "$sys" | sed 's/^/ /' echo "" + df_print_section "User" + local usr=$(systemctl --user --failed --no-pager --no-legend 2>/dev/null) + [[ -z "$usr" ]] && df_print_indent "✓ None" || echo "$usr" | sed 's/^/ /' } -# Show active timers sc-timers() { df_print_func_name "Active Timers" - - echo -e "${DF_CYAN}System Timers:${DF_NC}" - systemctl list-timers --no-pager | head -20 - - echo -e "\n${DF_CYAN}User Timers:${DF_NC}" + df_print_section "System" + systemctl list-timers --no-pager | head -15 + echo "" + df_print_section "User" systemctl --user list-timers --no-pager 2>/dev/null | head -10 - - echo "" } -# Show recently started/stopped services -sc-recent() { - local count="${1:-15}" - - df_print_func_name "Recent Service Activity" - - echo -e "${DF_CYAN}Recently Started:${DF_NC}" - systemctl list-units --type=service --state=running --no-pager --no-legend | \ - head -"$count" | awk '{print " " $1}' - - echo -e "\n${DF_CYAN}Recent Journal (services):${DF_NC}" - journalctl -p 3 -xb --no-pager | tail -"$count" | sed 's/^/ /' - - echo "" -} - -# Boot time analysis sc-boot() { - df_print_func_name "Boot Time Analysis" - - echo -e "${DF_CYAN}Boot Summary:${DF_NC}" + df_print_func_name "Boot Analysis" + df_print_section "Summary" systemd-analyze - - echo -e "\n${DF_CYAN}Slowest Services (top 10):${DF_NC}" + echo "" + df_print_section "Slowest (top 10)" systemd-analyze blame --no-pager | head -10 | sed 's/^/ /' - - echo -e "\n${DF_CYAN}Critical Chain:${DF_NC}" - systemd-analyze critical-chain --no-pager 2>/dev/null | head -15 | sed 's/^/ /' - - echo "" } -# ============================================================================ -# Service Search and Info -# ============================================================================ - -# Search for services by name sc-search() { - local query="$1" - [[ -z "$query" ]] && { echo "Usage: sc-search "; return 1; } - - df_print_func_name "Service Search: $query" - - systemctl list-unit-files --type=service --no-pager | grep -i "$query" + [[ -z "$1" ]] && { echo "Usage: sc-search "; return 1; } + df_print_func_name "Service Search: $1" + systemctl list-unit-files --type=service --no-pager | grep -i "$1" } -# Show detailed service info sc-info() { - local service="$1" - [[ -z "$service" ]] && { echo "Usage: sc-info "; return 1; } - - df_print_func_name "Service Info: $service" - - echo -e "${DF_CYAN}Status:${DF_NC}" - systemctl status "$service" --no-pager -l 2>/dev/null || \ - sudo systemctl status "$service" --no-pager -l - - echo -e "\n${DF_CYAN}Unit File:${DF_NC}" - systemctl cat "$service" 2>/dev/null | head -30 - + [[ -z "$1" ]] && { echo "Usage: sc-info "; return 1; } + df_print_func_name "Service: $1" + systemctl status "$1" --no-pager -l 2>/dev/null || sudo systemctl status "$1" --no-pager -l echo "" + df_print_section "Unit File" + systemctl cat "$1" 2>/dev/null | head -30 } -# ============================================================================ -# Quick Status for MOTD Integration -# ============================================================================ - -# Get count of failed services (for MOTD/prompt) -_systemd_failed_count() { - local count=$(systemctl --failed --no-pager --no-legend 2>/dev/null | wc -l) - echo "$count" -} - -# Check if a service is active (for scripts) -_systemd_is_active() { - local service="$1" - systemctl is-active --quiet "$service" 2>/dev/null -} - -# Check if a service is enabled (for scripts) -_systemd_is_enabled() { - local service="$1" - systemctl is-enabled --quiet "$service" 2>/dev/null -} - -# ============================================================================ -# Interactive Service Management (requires fzf) -# ============================================================================ - -if command -v fzf &>/dev/null; then - # Interactive service selector +# fzf interactive +if df_cmd_exists fzf; then scf() { - local service=$(systemctl list-units --type=service --no-pager --no-legend | \ - awk '{print $1, $2, $3, $4}' | \ - fzf --height=50% --layout=reverse --border=rounded \ - --prompt='Service > ' \ - --preview='systemctl status {1} --no-pager' \ - --preview-window=right:50%:wrap | \ - awk '{print $1}') - - if [[ -n "$service" ]]; then - echo -e "${DF_BLUE}Selected:${DF_NC} $service" - echo "" - echo "Actions: [s]tatus [r]estart [o]stop [l]ogs [e]nable [d]isable [q]uit" - read -k 1 "action?Action: " - echo "" - - case "$action" in - s) sudo systemctl status "$service" --no-pager -l ;; - r) scr "$service" ;; - o) sudo systemctl stop "$service" ;; - l) sclog "$service" ;; - e) sce "$service" ;; - d) scd "$service" ;; - q) return 0 ;; - *) echo "Unknown action" ;; - esac - fi - } - - # Interactive log viewer - sclogf() { - local service=$(systemctl list-units --type=service --no-pager --no-legend | \ - awk '{print $1}' | \ - fzf --height=40% --layout=reverse --prompt='Service logs > ') - - [[ -n "$service" ]] && sclog "$service" + local svc=$(systemctl list-units --type=service --no-pager --no-legend | \ + fzf $(df_fzf_opts) --prompt='Service > ' --preview='systemctl status {1} --no-pager' | awk '{print $1}') + [[ -z "$svc" ]] && return + df_print_info "Selected: $svc" + echo "[s]tatus [r]estart [l]ogs [e]nable [d]isable" + read -k 1 "act?Action: "; echo + case "$act" in + s) sudo systemctl status "$svc" --no-pager -l ;; + r) scr "$svc" ;; + l) sclog "$svc" ;; + e) sce "$svc" ;; + d) scd "$svc" ;; + esac } fi -# ============================================================================ -# Aliases -# ============================================================================ - -alias scs='sc status' -alias scstart='sc start' -alias scstop='sc stop' -alias screload='sc daemon-reload' -alias scmask='sc mask' -alias scunmask='sc unmask' - -# Journal shortcuts -alias jctl='journalctl' -alias jctlf='journalctl -f' -alias jctlb='journalctl -b' -alias jctlerr='journalctl -p err -b' - -# ============================================================================ -# Help -# ============================================================================ - sc-help() { - df_print_func_name "Systemd Helper Commands" - + df_print_func_name "Systemd Commands" cat << 'EOF' - - Core Commands: - sc sudo systemctl - scu systemctl --user - scr Restart and show status - sce Enable and start (--now) - scd Disable and stop (--now) - sclog Follow journal logs (-f) - sclogs Show recent logs (no follow) - - Status Commands: - sc-failed Show failed services - sc-timers Show active timers - sc-recent Recently started services - sc-boot Boot time analysis - sc-search Search services by name - sc-info Detailed service info - - Interactive (requires fzf): - scf Interactive service manager - sclogf Interactive log viewer - - Aliases: - scs sc status - scstart sc start - scstop sc stop - screload sc daemon-reload - - Journal: - jctl journalctl - jctlf journalctl -f - jctlb journalctl -b (current boot) - jctlerr journalctl -p err -b - + sc sudo systemctl + scu systemctl --user + scr Restart + status + sce Enable + start + scd Disable + stop + sclog Follow logs + sclogs Recent logs + sc-failed Failed services + sc-timers Active timers + sc-boot Boot analysis + sc-search Search services + sc-info Service details + scf Interactive (fzf) EOF } + +alias scs='sc status' scstart='sc start' scstop='sc stop' screload='sc daemon-reload' +alias jctl='journalctl' jctlf='journalctl -f' jctlb='journalctl -b' diff --git a/zsh/functions/tmux-workspaces.zsh b/zsh/functions/tmux-workspaces.zsh index af9fc17..ae808ae 100644 --- a/zsh/functions/tmux-workspaces.zsh +++ b/zsh/functions/tmux-workspaces.zsh @@ -1,380 +1,116 @@ # ============================================================================ # Tmux Workspace Manager - Project Templates & Layouts # ============================================================================ -# Quick project workspace setup with pre-configured tmux layouts -# ============================================================================ -# Source shared colors (with fallback) -source "${0:A:h}/../lib/colors.zsh" 2>/dev/null || \ -source "$HOME/.dotfiles/zsh/lib/colors.zsh" 2>/dev/null || { - typeset -g DF_GREEN=$'\033[0;32m' DF_BLUE=$'\033[0;34m' - typeset -g DF_YELLOW=$'\033[1;33m' DF_CYAN=$'\033[0;36m' - typeset -g DF_RED=$'\033[0;31m' DF_NC=$'\033[0m' +source "${0:A:h}/../lib/utils.zsh" 2>/dev/null || \ +source "$HOME/.dotfiles/zsh/lib/utils.zsh" 2>/dev/null + +typeset -g TW_TEMPLATES="${TW_TEMPLATES:-$HOME/.dotfiles/.tmux-templates}" +typeset -g TW_PREFIX="${TW_PREFIX:-work}" +typeset -g TW_DEFAULT="${TW_DEFAULT:-dev}" + +_tw_check() { df_require_cmd tmux || return 1; } + +_tw_init() { + df_ensure_dir "$TW_TEMPLATES" + [[ ! -f "$TW_TEMPLATES/dev.tmux" ]] && { + echo -e "# Dev layout\nsplit-window -h -p 50\nsplit-window -v -p 50\nselect-pane -t 0" > "$TW_TEMPLATES/dev.tmux" + echo -e "# Ops layout\nsplit-window -h\nsplit-window -v\nselect-pane -t 0\nsplit-window -v\nselect-pane -t 0" > "$TW_TEMPLATES/ops.tmux" + echo "# Full\n" > "$TW_TEMPLATES/full.tmux" + df_print_success "Created default templates" + } } -# ============================================================================ -# Configuration -# ============================================================================ - -typeset -g TW_TEMPLATES_DIR="${TW_TEMPLATES_DIR:-$HOME/.dotfiles/.tmux-templates}" -typeset -g TW_SESSION_PREFIX="${TW_SESSION_PREFIX:-work}" -typeset -g TW_DEFAULT_TEMPLATE="${TW_DEFAULT_TEMPLATE:-dev}" - -# ============================================================================ -# Helper Functions -# ============================================================================ - -_tw_print_step() { echo -e "${DF_BLUE}==>${DF_NC} $1"; } -_tw_print_success() { echo -e "${DF_GREEN}✓${DF_NC} $1"; } -_tw_print_error() { echo -e "${DF_RED}✗${DF_NC} $1"; } -_tw_print_info() { echo -e "${DF_CYAN}ℹ${DF_NC} $1"; } - -_tw_check_tmux() { - if ! command -v tmux &>/dev/null; then - _tw_print_error "tmux not installed" - return 1 - fi - return 0 -} - -_tw_init_templates() { - mkdir -p "$TW_TEMPLATES_DIR" - [[ ! -f "$TW_TEMPLATES_DIR/dev.tmux" ]] && _tw_create_default_templates -} - -# ============================================================================ -# Default Template Definitions -# ============================================================================ - -_tw_create_default_templates() { - _tw_print_step "Creating default templates..." - - cat > "$TW_TEMPLATES_DIR/dev.tmux" << 'EOF' -# Development workspace -split-window -h -p 50 -split-window -v -p 50 -select-pane -t 0 -EOF - - cat > "$TW_TEMPLATES_DIR/ops.tmux" << 'EOF' -# Operations workspace - 4 panes -split-window -h -p 50 -split-window -v -p 50 -select-pane -t 0 -split-window -v -p 50 -select-pane -t 0 -EOF - - cat > "$TW_TEMPLATES_DIR/ssh-multi.tmux" << 'EOF' -# Multi-server SSH workspace -split-window -h -p 50 -split-window -v -p 50 -select-pane -t 0 -split-window -v -p 50 -select-pane -t 0 -EOF - - cat > "$TW_TEMPLATES_DIR/debug.tmux" << 'EOF' -# Debug workspace -split-window -h -p 30 -select-pane -t 0 -EOF - - cat > "$TW_TEMPLATES_DIR/full.tmux" << 'EOF' -# Full workspace - single pane -EOF - - cat > "$TW_TEMPLATES_DIR/review.tmux" << 'EOF' -# Code Review workspace -split-window -h -p 50 -select-pane -t 0 -EOF - - _tw_print_success "Created default templates in: $TW_TEMPLATES_DIR" -} - -# ============================================================================ -# Template Management -# ============================================================================ - tw-templates() { - _tw_init_templates - - df_print_func_name "Available Tmux Templates" - - for template in "$TW_TEMPLATES_DIR"/*.tmux; do - [[ ! -f "$template" ]] && continue - local name=$(basename "$template" .tmux) - local description=$(grep "^#" "$template" | head -2 | tail -1 | sed 's/^# *//') - echo -e "${DF_GREEN}●${DF_NC} ${DF_CYAN}$name${DF_NC}" - [[ -n "$description" ]] && echo " $description" + _tw_init + df_print_func_name "Tmux Templates" + for t in "$TW_TEMPLATES"/*.tmux; do + [[ -f "$t" ]] && df_print_indent "● $(basename "$t" .tmux)" done - - echo - echo "Create workspace: ${DF_CYAN}tw-create myproject dev${DF_NC}" - echo "Quick attach: ${DF_CYAN}tw myproject${DF_NC}" + echo "" + df_print_info "Create: tw-create