diff --git a/.ssh-profiles b/.ssh-profiles new file mode 100644 index 0000000..5616b24 --- /dev/null +++ b/.ssh-profiles @@ -0,0 +1,2 @@ +# SSH Connection Profiles +# Format: name|user@host|port|key_file|options|description diff --git a/bin/dotfiles-doctor.sh b/bin/dotfiles-doctor.sh index 3c9df26..2f78053 100755 --- a/bin/dotfiles-doctor.sh +++ b/bin/dotfiles-doctor.sh @@ -2,16 +2,40 @@ # ============================================================================ # Dotfiles Health Check (Arch/CachyOS) # ============================================================================ +# Comprehensive health check with Arch-specific diagnostics +# +# Usage: +# dotfiles-doctor.sh # Run all checks +# dotfiles-doctor.sh --fix # Attempt automatic fixes +# dotfiles-doctor.sh --quick # Quick essential checks only +# ============================================================================ -# Note: Not using set -e because arithmetic operations like ((var++)) -# return 1 when var was 0, which would cause premature exit +# Note: Not using set -e because arithmetic operations can return non-zero readonly DOTFILES_HOME="${DOTFILES_HOME:-$HOME/.dotfiles}" -readonly DOTFILES_VERSION="3.0.0" +readonly DOTFILES_VERSION="3.1.0" + +# Parse arguments +DO_FIX=false +QUICK_MODE=false +for arg in "$@"; do + case "$arg" in + --fix) DO_FIX=true ;; + --quick) QUICK_MODE=true ;; + --help|-h) + echo "Usage: dotfiles-doctor.sh [OPTIONS]" + echo "" + echo "Options:" + echo " --fix Attempt automatic fixes for issues" + echo " --quick Run quick essential checks only" + echo " --help Show this help" + exit 0 + ;; + esac +done # Source shared colors source "$DOTFILES_HOME/zsh/lib/colors.zsh" 2>/dev/null || { - # Fallback if colors.zsh not found DF_RED=$'\033[0;31m' DF_GREEN=$'\033[0;32m' DF_YELLOW=$'\033[1;33m' DF_BLUE=$'\033[0;34m' DF_CYAN=$'\033[0;36m' DF_NC=$'\033[0m' DF_GREY=$'\033[38;5;242m' DF_LIGHT_BLUE=$'\033[38;5;39m' @@ -23,27 +47,24 @@ TOTAL_CHECKS=0 PASSED_CHECKS=0 FAILED_CHECKS=0 WARNING_CHECKS=0 +FIXED_CHECKS=0 # ============================================================================ # MOTD-style header # ============================================================================ print_header() { - if declare -f df_print_header &>/dev/null; then - df_print_header "dotfiles-doctor" - else - local user="${USER:-root}" - local hostname="${HOSTNAME:-$(hostname -s 2>/dev/null)}" - local datetime=$(date '+%a %b %d %H:%M') - local width=66 - local hline="" && for ((i=0; i/dev/null; then - check_pass "Running on Arch/CachyOS" + if [[ "$OSTYPE" == "linux-gnu"* ]]; then + if grep -qi "cachyos" /etc/os-release 2>/dev/null; then + local version=$(grep "VERSION_ID" /etc/os-release 2>/dev/null | cut -d= -f2 | tr -d '"') + check_pass "Running CachyOS ${version}" + elif grep -qi "arch" /etc/os-release 2>/dev/null; then + check_pass "Running Arch Linux" else check_fail "Not running on Arch/CachyOS" fi else check_fail "Not running on Linux" fi + + # Kernel check + local kernel=$(uname -r) + if [[ "$kernel" == *"cachyos"* ]]; then + check_pass "CachyOS kernel: $kernel" + elif [[ "$kernel" == *"zen"* ]]; then + check_pass "Zen kernel: $kernel" + elif [[ "$kernel" == *"lts"* ]]; then + check_pass "LTS kernel: $kernel" + else + check_pass "Kernel: $kernel" + fi } check_shell() { @@ -97,12 +138,24 @@ check_shell() { check_pass "Zsh configuration exists" else check_fail "Zsh configuration missing" + if [[ "$DO_FIX" == true ]]; then + ln -sf "$DOTFILES_HOME/zsh/.zshrc" "$HOME/.zshrc" 2>/dev/null && check_fixed ".zshrc symlink created" + fi fi if [[ "$SHELL" == *"zsh"* ]]; then check_pass "Zsh is default shell" else check_warn "Zsh is not default shell (current: $SHELL)" + if [[ "$DO_FIX" == true ]]; then + echo " Run: chsh -s \$(which zsh)" + fi + fi + + # Check if zsh is recent version + if command -v zsh &>/dev/null; then + local zsh_version=$(zsh --version | awk '{print $2}') + check_pass "Zsh version: $zsh_version" fi } @@ -114,13 +167,15 @@ check_symlinks() { for symlink in ~/.zshrc ~/.gitconfig ~/.vimrc ~/.tmux.conf; do if [[ -L "$symlink" ]]; then - ((symlink_count++)) + symlink_count=$((symlink_count + 1)) if [[ -e "$symlink" ]]; then check_pass "$(basename $symlink) → $(readlink $symlink)" else - ((broken_count++)) + broken_count=$((broken_count + 1)) check_fail "$(basename $symlink) is broken" fi + elif [[ -f "$symlink" ]]; then + check_warn "$(basename $symlink) is regular file (not symlink)" fi done @@ -132,15 +187,15 @@ check_symlinks() { check_vim() { print_section "Editor Configuration" - if command -v vim &> /dev/null; then - local vim_version=$(vim --version | head -1) + if command -v vim &>/dev/null; then + local vim_version=$(vim --version | head -1 | awk '{print $5}') check_pass "Vim installed: $vim_version" else check_fail "Vim not installed" fi - if command -v nvim &> /dev/null; then - local nvim_version=$(nvim --version | head -1) + if command -v nvim &>/dev/null; then + local nvim_version=$(nvim --version | head -1 | awk '{print $2}') check_pass "Neovim installed: $nvim_version" else check_warn "Neovim not installed (optional)" @@ -150,19 +205,18 @@ check_vim() { check_git() { print_section "Git Configuration" - if command -v git &> /dev/null; then + if command -v git &>/dev/null; then check_pass "Git installed" - if git config --global user.name &> /dev/null; then + if git config --global user.name &>/dev/null; then local git_user=$(git config --global user.name) - check_pass "Git user configured: $git_user" + check_pass "Git user: $git_user" else check_fail "Git user not configured" fi - if git config --global user.email &> /dev/null; then - local git_email=$(git config --global user.email) - check_pass "Git email configured: $git_email" + if git config --global user.email &>/dev/null; then + check_pass "Git email configured" else check_fail "Git email not configured" fi @@ -171,53 +225,176 @@ check_git() { fi } -check_optional_tools() { - print_section "Optional Tools" - - if command -v fzf &> /dev/null; then - check_pass "fzf installed (fuzzy finder)" - else - check_warn "fzf not installed (command palette requires this)" - fi - - if command -v lastpass-cli &> /dev/null || command -v lpass &> /dev/null; then - check_pass "LastPass CLI installed" - else - check_warn "LastPass CLI not installed (password manager)" - fi - - if command -v tmux &> /dev/null; then - check_pass "Tmux installed" - else - check_warn "Tmux not installed (workspaces require this)" - fi - - if command -v age &> /dev/null || command -v gpg &> /dev/null; then - check_pass "Encryption tool available (age or gpg)" - else - check_warn "No encryption tool (vault requires age or gpg)" - fi - - if command -v bat &> /dev/null; then - check_pass "bat installed (syntax highlighting)" - else - check_warn "bat not installed (optional enhancement)" - fi - - if command -v eza &> /dev/null; then - check_pass "eza installed (ls replacement)" - else - check_warn "eza not installed (optional enhancement)" - fi -} +# ============================================================================ +# Arch-Specific Checks +# ============================================================================ check_pacman() { print_section "Package Manager" - if command -v pacman &> /dev/null; then + if command -v pacman &>/dev/null; then check_pass "Pacman available" else - check_fail "Pacman not found (this is Arch/CachyOS only)" + check_fail "Pacman not found" + return + fi + + # Check for AUR helper + if command -v paru &>/dev/null; then + check_pass "AUR helper: paru" + elif command -v yay &>/dev/null; then + check_pass "AUR helper: yay" + else + check_warn "No AUR helper installed (recommend: paru)" + fi +} + +check_pacman_health() { + [[ "$QUICK_MODE" == true ]] && return + + print_section "Pacman Health" + + # Check for orphaned packages + local orphans=$(pacman -Qtdq 2>/dev/null | wc -l) + if [[ $orphans -eq 0 ]]; then + check_pass "No orphaned packages" + else + check_warn "$orphans orphaned package(s)" + if [[ "$DO_FIX" == true ]]; then + echo " Clean: pacman -Qtdq | sudo pacman -Rns -" + fi + fi + + # Check package cache size + if [[ -d /var/cache/pacman/pkg ]]; then + local cache_size=$(du -sh /var/cache/pacman/pkg 2>/dev/null | cut -f1) + local pkg_count=$(ls /var/cache/pacman/pkg 2>/dev/null | wc -l) + + if [[ $pkg_count -gt 500 ]]; then + check_warn "Package cache: $cache_size ($pkg_count files)" + if [[ "$DO_FIX" == true ]]; then + echo " Clean: sudo paccache -rk2" + fi + else + check_pass "Package cache: $cache_size" + fi + fi + + # Check for available updates + if command -v checkupdates &>/dev/null; then + local updates=$(checkupdates 2>/dev/null | wc -l) + if [[ $updates -eq 0 ]]; then + check_pass "System up to date" + else + check_warn "$updates update(s) available" + fi + fi +} + +check_systemd() { + [[ "$QUICK_MODE" == true ]] && return + + print_section "Systemd Services" + + # Check for failed services + local failed_count=$(systemctl --failed --no-pager --no-legend 2>/dev/null | wc -l) + + if [[ $failed_count -eq 0 ]]; then + check_pass "No failed system services" + else + check_fail "$failed_count failed service(s)" + systemctl --failed --no-pager --no-legend 2>/dev/null | head -3 | while read -r line; do + local svc=$(echo "$line" | awk '{print $1}') + echo -e " ${DF_DIM}• $svc${DF_NC}" + done + fi + + # Check user services + local user_failed=$(systemctl --user --failed --no-pager --no-legend 2>/dev/null | wc -l) + if [[ $user_failed -eq 0 ]]; then + check_pass "No failed user services" + else + check_warn "$user_failed failed user service(s)" + fi +} + +check_btrfs() { + [[ "$QUICK_MODE" == true ]] && return + + # Only check if root is btrfs + local fstype=$(df -T / 2>/dev/null | awk 'NR==2 {print $2}') + [[ "$fstype" != "btrfs" ]] && return + + print_section "Btrfs Filesystem" + + check_pass "Root filesystem: btrfs" + + # Check for device errors + local stats=$(sudo btrfs device stats / 2>/dev/null) + local errors=$(echo "$stats" | grep -v " 0$" | grep -v "^$") + + if [[ -z "$errors" ]]; then + check_pass "No btrfs device errors" + else + check_fail "Btrfs errors detected!" + echo "$errors" | head -3 | while read -r line; do + echo -e " ${DF_DIM}$line${DF_NC}" + done + fi + + # Check last scrub + local scrub_info=$(sudo btrfs scrub status / 2>/dev/null) + if echo "$scrub_info" | grep -q "running"; then + check_pass "Scrub currently running" + elif echo "$scrub_info" | grep -q "finished"; then + local scrub_date=$(echo "$scrub_info" | grep "Scrub started" | awk '{print $3, $4}') + check_pass "Last scrub: $scrub_date" + else + check_warn "No scrub history (recommend monthly)" + fi + + # Check snapper + if command -v snapper &>/dev/null && [[ -d "/.snapshots" ]]; then + local snap_count=$(sudo snapper -c root list 2>/dev/null | tail -n +3 | wc -l) + check_pass "Snapper: $snap_count snapshot(s)" + fi +} + +# ============================================================================ +# Standard Checks +# ============================================================================ + +check_optional_tools() { + print_section "Optional Tools" + + if command -v fzf &>/dev/null; then + check_pass "fzf (fuzzy finder)" + else + check_warn "fzf not installed (command palette needs this)" + fi + + if command -v bat &>/dev/null; then + check_pass "bat (syntax highlighting)" + else + check_warn "bat not installed" + fi + + if command -v eza &>/dev/null; then + check_pass "eza (modern ls)" + else + check_warn "eza not installed" + fi + + if command -v tmux &>/dev/null; then + check_pass "tmux (terminal multiplexer)" + else + check_warn "tmux not installed" + fi + + if command -v age &>/dev/null || command -v gpg &>/dev/null; then + check_pass "Encryption available (age/gpg)" + else + check_warn "No encryption tool (vault needs age/gpg)" fi } @@ -229,15 +406,23 @@ check_permissions() { check_pass "install.sh is executable" else check_fail "install.sh is not executable" + if [[ "$DO_FIX" == true ]]; then + chmod +x "$DOTFILES_HOME/install.sh" + check_fixed "install.sh permissions" + fi fi fi if [[ -d "$DOTFILES_HOME/bin" ]]; then local non_exec=$(find "$DOTFILES_HOME/bin" -type f ! -perm /u+x 2>/dev/null | wc -l) if [[ $non_exec -eq 0 ]]; then - check_pass "All scripts in bin/ are executable" + check_pass "All bin/ scripts executable" else - check_fail "$non_exec scripts in bin/ are not executable" + check_fail "$non_exec bin/ scripts not executable" + if [[ "$DO_FIX" == true ]]; then + find "$DOTFILES_HOME/bin" -type f ! -perm /u+x -exec chmod +x {} \; + check_fixed "bin/ permissions" + fi fi fi } @@ -249,19 +434,19 @@ check_zsh_plugins() { check_pass "Oh My Zsh installed" if [[ -d "$HOME/.oh-my-zsh/custom/plugins/zsh-autosuggestions" ]]; then - check_pass "zsh-autosuggestions installed" + check_pass "zsh-autosuggestions" else check_warn "zsh-autosuggestions not installed" fi if [[ -d "$HOME/.oh-my-zsh/custom/plugins/zsh-syntax-highlighting" ]]; then - check_pass "zsh-syntax-highlighting installed" + check_pass "zsh-syntax-highlighting" else check_warn "zsh-syntax-highlighting not installed" fi if [[ -f "$HOME/.oh-my-zsh/themes/adlee.zsh-theme" ]]; then - check_pass "adlee theme installed" + check_pass "adlee theme" else check_warn "adlee theme not installed" fi @@ -274,27 +459,33 @@ check_dotfiles_dir() { print_section "Dotfiles Directory" if [[ -d "$DOTFILES_HOME" ]]; then - check_pass "Dotfiles directory found: $DOTFILES_HOME" + check_pass "Dotfiles: $DOTFILES_HOME" else - check_fail "Dotfiles directory not found: $DOTFILES_HOME" + check_fail "Dotfiles not found: $DOTFILES_HOME" return fi if [[ -f "$DOTFILES_HOME/dotfiles.conf" ]]; then - check_pass "Configuration file exists" + check_pass "Config file exists" else - check_warn "Configuration file missing" + check_warn "Config file missing" fi if [[ -d "$DOTFILES_HOME/.git" ]]; then - check_pass "Git repository initialized" + check_pass "Git repo initialized" + + # Check for uncommitted changes + local changes=$(cd "$DOTFILES_HOME" && git status --porcelain 2>/dev/null | wc -l) + if [[ $changes -gt 0 ]]; then + check_warn "$changes uncommitted change(s)" + fi else check_warn "Not a git repository" fi } # ============================================================================ -# Print summary +# Print Summary # ============================================================================ print_summary() { @@ -304,21 +495,29 @@ print_summary() { if [[ $FAILED_CHECKS -eq 0 ]]; then echo -e "${DF_GREEN}✓${DF_NC} All checks passed ($PASSED_CHECKS/$TOTAL_CHECKS)" else - echo -e "${DF_RED}✗${DF_NC} Some checks failed" - echo -e " ${DF_GREEN}Passed:${DF_NC} $PASSED_CHECKS" - echo -e " ${DF_RED}Failed:${DF_NC} $FAILED_CHECKS" + echo -e "${DF_RED}✗${DF_NC} Issues found" + echo -e " ${DF_GREEN}Passed:${DF_NC} $PASSED_CHECKS" + echo -e " ${DF_RED}Failed:${DF_NC} $FAILED_CHECKS" if [[ $WARNING_CHECKS -gt 0 ]]; then echo -e " ${DF_YELLOW}Warnings:${DF_NC} $WARNING_CHECKS" fi + if [[ $FIXED_CHECKS -gt 0 ]]; then + echo -e " ${DF_CYAN}Fixed:${DF_NC} $FIXED_CHECKS" + fi fi echo "" - if [[ $FAILED_CHECKS -gt 0 ]]; then - echo -e "${DF_YELLOW}💡 Tip:${DF_NC} Run 'dotfiles-doctor.sh --fix' to attempt automatic fixes" + if [[ $FAILED_CHECKS -gt 0 && "$DO_FIX" != true ]]; then + echo -e "${DF_YELLOW}💡${DF_NC} Run with --fix to attempt automatic fixes" echo "" return 1 fi + + if [[ $FIXED_CHECKS -gt 0 ]]; then + echo -e "${DF_CYAN}ℹ${DF_NC} Fixed $FIXED_CHECKS issue(s). Run again to verify." + echo "" + fi } # ============================================================================ @@ -328,16 +527,24 @@ print_summary() { main() { print_header + # Essential checks (always run) check_os check_pacman check_shell - check_vim - check_git check_dotfiles_dir check_symlinks - check_zsh_plugins - check_optional_tools - check_permissions + + # Additional checks (skip in quick mode) + if [[ "$QUICK_MODE" != true ]]; then + check_vim + check_git + check_zsh_plugins + check_optional_tools + check_permissions + check_pacman_health + check_systemd + check_btrfs + fi print_summary } diff --git a/dotfiles.conf b/dotfiles.conf index 7b67304..60ce6ec 100644 --- a/dotfiles.conf +++ b/dotfiles.conf @@ -43,7 +43,7 @@ SET_ZSH_DEFAULT="ask" # --- MOTD (Message of the Day) --- ENABLE_MOTD="true" -MOTD_STYLE="compact" +MOTD_STYLE="compact" # compact, mini, full, or none # --- Theme Settings --- ZSH_THEME_NAME="adlee" @@ -51,11 +51,11 @@ THEME_TIMER_THRESHOLD=10 THEME_PATH_TRUNCATE_LENGTH=32 # --- Advanced Features --- -ENABLE_SMART_SUGGESTIONS="true" # Typo correction -ENABLE_COMMAND_PALETTE="true" # Ctrl+Space launcher -ENABLE_SHELL_ANALYTICS="false" # Command usage stats -ENABLE_VAULT="true" # Encrypted secrets storage -DOTFILES_AUTO_SYNC_CHECK="true" # Check for updates on shell start +ENABLE_SMART_SUGGESTIONS="true" # Typo correction +ENABLE_COMMAND_PALETTE="true" # Ctrl+Space launcher +ENABLE_SHELL_ANALYTICS="false" # Command usage stats +ENABLE_VAULT="true" # Encrypted secrets storage +DOTFILES_AUTO_SYNC_CHECK="true" # Check for updates on shell start # --- Snapper Settings (Arch/CachyOS only) --- SNAPPER_CONFIG="root" @@ -77,6 +77,30 @@ SSH_AUTO_TMUX="true" SSH_TMUX_SESSION_PREFIX="ssh" SSH_SYNC_DOTFILES="ask" +# ============================================================================ +# Btrfs Helpers (btrfs-helpers.zsh) +# ============================================================================ + +# Default mount point for btrfs commands +BTRFS_DEFAULT_MOUNT="/" + +# ============================================================================ +# Systemd Helpers (systemd-helpers.zsh) +# ============================================================================ + +# Show failed services count in MOTD +MOTD_SHOW_FAILED_SERVICES="true" + +# ============================================================================ +# Package Manager +# ============================================================================ + +# Show available updates in MOTD +MOTD_SHOW_UPDATES="true" + +# Preferred AUR helper: paru, yay, or auto (auto-detect) +AUR_HELPER="auto" + # ============================================================================ # Derived URLs (generally don't edit these) # ============================================================================ diff --git a/zsh/.zshrc b/zsh/.zshrc index 8363497..d77513d 100644 --- a/zsh/.zshrc +++ b/zsh/.zshrc @@ -244,15 +244,26 @@ if _has_cmd kubectl; then fi # ============================================================================ -# Dotfiles Functions (deferred loading) +# Dotfiles Configuration # ============================================================================ _dotfiles_dir="$HOME/.dotfiles" +# Load dotfiles.conf first (sets DOTFILES_DIR and other vars) +if [[ -f "$_dotfiles_dir/dotfiles.conf" ]]; then + source "$_dotfiles_dir/dotfiles.conf" +else + DOTFILES_DIR="$HOME/.dotfiles" + DOTFILES_BRANCH="main" +fi + +# Source shared colors library +[[ -f "$_dotfiles_dir/zsh/lib/colors.zsh" ]] && source "$_dotfiles_dir/zsh/lib/colors.zsh" + # Source dotfiles aliases [[ -f "$_dotfiles_dir/zsh/aliases.zsh" ]] && source "$_dotfiles_dir/zsh/aliases.zsh" -# These are loaded immediately (small files, needed for keybindings) +# Load command-palette immediately (needed for keybindings) [[ -f "$_dotfiles_dir/zsh/functions/command-palette.zsh" ]] && \ source "$_dotfiles_dir/zsh/functions/command-palette.zsh" @@ -267,34 +278,24 @@ _deferred_load() { # Setup FZF _has_cmd fzf && _setup_fzf - # Source optional function files - [[ -f "$_dotfiles_dir/zsh/functions/snapper.zsh" ]] && \ - source "$_dotfiles_dir/zsh/functions/snapper.zsh" - [[ -f "$_dotfiles_dir/zsh/functions/smart-suggest.zsh" ]] && \ - source "$_dotfiles_dir/zsh/functions/smart-suggest.zsh" - [[ -f "$_dotfiles_dir/zsh/functions/password-manager.zsh" ]] && \ - source "$_dotfiles_dir/zsh/functions/password-manager.zsh" - [[ -f "$_dotfiles_dir/zsh/functions/tmux-workspaces.zsh" ]] && \ - source "$_dotfiles_dir/zsh/functions/tmux-workspaces.zsh" - [[ -f "$_dotfiles_dir/zsh/functions/python-templates.zsh" ]] && \ - source "$_dotfiles_dir/zsh/functions/python-templates.zsh" - - # Load vault secrets - #local vault_script="$_dotfiles_dir/bin/dotfiles-vault.sh" - #if [[ -f "$_dotfiles_dir/vault/secrets.enc" ]] && [[ -x "$vault_script" ]]; then - # eval "$("$vault_script" shell 2>/dev/null)" || true - #fi - - # Load dotfiles.conf env variables. - DOTFILES_CONF="$HOME/.dotfiles/dotfiles.conf" - - if [[ -f "$DOTFILES_CONF" ]]; then - source $DOTFILES_CONF - else - DOTFILES_DIR="$HOME/.dotfiles" - DOTFILES_BRANCH="main" + # ----------------------------------------------------------------------- + # Load all function files from functions directory + # Excludes command-palette.zsh (already loaded) and motd.zsh (loaded separately) + # ----------------------------------------------------------------------- + local func_dir="$_dotfiles_dir/zsh/functions" + if [[ -d "$func_dir" ]]; then + for func_file in "$func_dir"/*.zsh; do + [[ -f "$func_file" ]] || continue + + # Skip files that are loaded elsewhere + case "${func_file:t}" in + command-palette.zsh) continue ;; # Loaded early for keybindings + motd.zsh) continue ;; # Loaded after prompt + esac + + source "$func_file" + done fi - } # ============================================================================ @@ -304,18 +305,20 @@ _deferred_load() { _background_tasks() { # Check for dotfiles updates if [[ "${DOTFILES_AUTO_SYNC_CHECK:-true}" == "true" ]]; then - # Use full path to avoid command_not_found issues $_dotfiles_dir/bin/dotfiles-sync.sh status -s 2> /dev/null - #[[ -x "$sync_script" ]] && "$sync_script" --auto 2>/dev/null &! fi _df_check_sys_updates - } _df_check_sys_updates() { - # Check number of available updates and export. - export UPDATE_PKG_COUNT=$(checkupdates | wc -l) + # Check number of available updates and export + if _has_cmd checkupdates; then + export UPDATE_PKG_COUNT=$(checkupdates 2>/dev/null | wc -l) + else + export UPDATE_PKG_COUNT=0 + fi } + # ============================================================================ # Initialization Strategy # ============================================================================ @@ -344,6 +347,7 @@ else case "${MOTD_STYLE:-compact}" in compact) show_motd ;; mini) show_motd_mini ;; + full) show_motd_full ;; esac fi diff --git a/zsh/functions/btrfs-helpers.zsh b/zsh/functions/btrfs-helpers.zsh new file mode 100644 index 0000000..9e0d1d0 --- /dev/null +++ b/zsh/functions/btrfs-helpers.zsh @@ -0,0 +1,441 @@ +# ============================================================================ +# 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 +# ============================================================================ + +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" + 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}" + + echo -e "${DF_BLUE}╔════════════════════════════════════════════════════════════╗${DF_NC}" + echo -e "${DF_BLUE}║${DF_NC} Btrfs Filesystem Usage: ${mount} " + echo -e "${DF_BLUE}╚════════════════════════════════════════════════════════════╝${DF_NC}" + echo "" + + sudo btrfs filesystem usage "$mount" -h +} + +# List all subvolumes +btrfs-subs() { + _btrfs_check || return 1 + local mount="${1:-$BTRFS_DEFAULT_MOUNT}" + + echo -e "${DF_BLUE}╔════════════════════════════════════════════════════════════╗${DF_NC}" + echo -e "${DF_BLUE}║${DF_NC} Btrfs Subvolumes " + echo -e "${DF_BLUE}╚════════════════════════════════════════════════════════════╝${DF_NC}" + echo "" + + echo -e "${DF_CYAN}Subvolume List:${DF_NC}" + 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" + done + + echo "" + echo -e "${DF_CYAN}Default Subvolume:${DF_NC}" + 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 + + echo -e "${DF_BLUE}==>${DF_NC} Starting btrfs balance on ${mount}" + echo -e "${DF_YELLOW}⚠${DF_NC} This may take a while and use significant I/O" + 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..." + 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 +} + +# Check balance status +btrfs-balance-status() { + _btrfs_check || return 1 + local mount="${1:-$BTRFS_DEFAULT_MOUNT}" + + sudo btrfs balance status "$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" +} + +# Start scrub operation +btrfs-scrub() { + _btrfs_check || return 1 + local mount="${1:-$BTRFS_DEFAULT_MOUNT}" + + echo -e "${DF_BLUE}╔════════════════════════════════════════════════════════════╗${DF_NC}" + echo -e "${DF_BLUE}║${DF_NC} Btrfs Scrub " + echo -e "${DF_BLUE}╚════════════════════════════════════════════════════════════╝${DF_NC}" + echo "" + + # 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}" + 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..." + sudo btrfs scrub start "$mount" + + echo "" + echo -e "${DF_CYAN}Scrub Status:${DF_NC}" + sudo btrfs scrub status "$mount" + + echo "" + echo -e "${DF_CYAN}Monitor with:${DF_NC} btrfs-scrub-status" +} + +# Show scrub status +btrfs-scrub-status() { + _btrfs_check || return 1 + local mount="${1:-$BTRFS_DEFAULT_MOUNT}" + + sudo btrfs scrub status "$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" +} + +# 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 + + echo -e "${DF_BLUE}==>${DF_NC} Defragmenting: $target" + + if [[ -d "$target" ]]; then + echo -e "${DF_YELLOW}⚠${DF_NC} Recursive defrag on directory" + read -q "REPLY?Continue? [y/N]: "; echo + [[ ! "$REPLY" =~ ^[Yy]$ ]] && return 0 + + sudo btrfs filesystem defragment -r -v "$target" + else + sudo btrfs filesystem defragment -v "$target" + fi + + echo -e "${DF_GREEN}✓${DF_NC} 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 + + echo -e "${DF_BLUE}╔════════════════════════════════════════════════════════════╗${DF_NC}" + echo -e "${DF_BLUE}║${DF_NC} Btrfs Compression Statistics " + echo -e "${DF_BLUE}╚════════════════════════════════════════════════════════════╝${DF_NC}" + echo "" + + sudo compsize "$target" +} + +# ============================================================================ +# Information Commands +# ============================================================================ + +# Full filesystem info +btrfs-info() { + _btrfs_check || return 1 + local mount="${1:-$BTRFS_DEFAULT_MOUNT}" + + echo -e "${DF_BLUE}╔════════════════════════════════════════════════════════════╗${DF_NC}" + echo -e "${DF_BLUE}║${DF_NC} Btrfs Filesystem Information " + echo -e "${DF_BLUE}╚════════════════════════════════════════════════════════════╝${DF_NC}" + + echo -e "\n${DF_CYAN}Filesystem Show:${DF_NC}" + 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 "" +} + +# Quick health check +btrfs-health() { + _btrfs_check || return 1 + local mount="${1:-$BTRFS_DEFAULT_MOUNT}" + + echo -e "${DF_BLUE}╔════════════════════════════════════════════════════════════╗${DF_NC}" + echo -e "${DF_BLUE}║${DF_NC} Btrfs Health Check " + echo -e "${DF_BLUE}╚════════════════════════════════════════════════════════════╝${DF_NC}" + echo "" + + 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 '%') + + 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" + fi + + echo "" +} + +# ============================================================================ +# Snapshot Helpers (complement snapper.zsh) +# ============================================================================ + +# Show snapshot space usage +btrfs-snap-usage() { + _btrfs_check || return 1 + + echo -e "${DF_BLUE}╔════════════════════════════════════════════════════════════╗${DF_NC}" + echo -e "${DF_BLUE}║${DF_NC} Snapshot Space Usage " + echo -e "${DF_BLUE}╚════════════════════════════════════════════════════════════╝${DF_NC}" + echo "" + + if [[ -d "/.snapshots" ]]; then + echo -e "${DF_CYAN}Snapshot Directory:${DF_NC}" + sudo du -sh /.snapshots 2>/dev/null || echo " Unable to calculate" + + echo -e "\n${DF_CYAN}Individual Snapshots:${DF_NC}" + sudo du -sh /.snapshots/*/ 2>/dev/null | sort -h | tail -10 | sed 's/^/ /' + else + echo -e "${DF_YELLOW}⚠${DF_NC} No /.snapshots directory found" + fi + + echo "" +} + +# ============================================================================ +# Maintenance +# ============================================================================ + +# Full maintenance routine +btrfs-maintain() { + _btrfs_check || return 1 + local mount="${1:-$BTRFS_DEFAULT_MOUNT}" + + echo -e "${DF_BLUE}╔════════════════════════════════════════════════════════════╗${DF_NC}" + echo -e "${DF_BLUE}║${DF_NC} Btrfs Maintenance Routine " + echo -e "${DF_BLUE}╚════════════════════════════════════════════════════════════╝${DF_NC}" + echo "" + 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" + btrfs-health "$mount" + + echo -e "${DF_BLUE}==>${DF_NC} 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" +} + +# ============================================================================ +# Aliases +# ============================================================================ + +alias btru='btrfs-usage' +alias btrs='btrfs-subs' +alias btrh='btrfs-health' +alias btri='btrfs-info' +alias btrc='btrfs-compress' + +# ============================================================================ +# Help +# ============================================================================ + +btrfs-help() { + echo -e "${DF_BLUE}╔════════════════════════════════════════════════════════════╗${DF_NC}" + echo -e "${DF_BLUE}║${DF_NC} Btrfs Helper Commands " + echo -e "${DF_BLUE}╚════════════════════════════════════════════════════════════╝${DF_NC}" + + 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 + +EOF +} diff --git a/zsh/functions/motd.zsh b/zsh/functions/motd.zsh index 58164c3..fa40feb 100644 --- a/zsh/functions/motd.zsh +++ b/zsh/functions/motd.zsh @@ -3,10 +3,12 @@ # MOTD (Message of the Day) - Dynamic System Info # ============================================================================ # Displays system information on shell startup +# Optimized for Arch/CachyOS with direct /proc access # # Functions: # show_motd - Compact box format # show_motd_mini - Single line format +# show_motd_full - Extended info # ============================================================================ # Only run in interactive shells @@ -18,7 +20,7 @@ source "$HOME/.dotfiles/zsh/lib/colors.zsh" 2>/dev/null || { typeset -g DF_RESET=$'\033[0m' DF_BOLD=$'\033[1m' DF_DIM=$'\033[2m' typeset -g DF_BLUE=$'\033[38;5;39m' DF_CYAN=$'\033[38;5;51m' typeset -g DF_GREEN=$'\033[38;5;82m' DF_YELLOW=$'\033[38;5;220m' - typeset -g DF_GREY=$'\033[38;5;242m' DF_NC=$'\033[0m' + typeset -g DF_RED=$'\033[38;5;196m' DF_GREY=$'\033[38;5;242m' DF_NC=$'\033[0m' } # ============================================================================ @@ -28,32 +30,98 @@ source "$HOME/.dotfiles/zsh/lib/colors.zsh" 2>/dev/null || { typeset -g _M_WIDTH=66 # ============================================================================ -# Info Gathering +# Optimized Info Gathering (using /proc directly - faster than spawning processes) # ============================================================================ +# Uptime from /proc (no subprocess) _motd_uptime() { - local up=$(uptime 2>/dev/null) - if [[ "$up" =~ "up "([^,]+) ]]; then - echo "${match[1]}" | sed 's/^ *//' + local uptime_seconds=$(cut -d. -f1 /proc/uptime 2>/dev/null) + [[ -z "$uptime_seconds" ]] && { echo "?"; return; } + + local days=$((uptime_seconds / 86400)) + local hours=$(((uptime_seconds % 86400) / 3600)) + local mins=$(((uptime_seconds % 3600) / 60)) + + if (( days > 0 )); then + echo "${days}d ${hours}h" + elif (( hours > 0 )); then + echo "${hours}h ${mins}m" else - echo "?" + echo "${mins}m" fi } +# Load from /proc (no subprocess) _motd_load() { - if [[ -f /proc/loadavg ]]; then - awk '{print $1}' /proc/loadavg - else - uptime | awk -F'load average:' '{print $2}' | awk -F, '{print $1}' | xargs + cut -d' ' -f1 /proc/loadavg 2>/dev/null || echo "?" +} + +# Memory from /proc (single awk call) +_motd_mem() { + awk '/MemTotal/ {total=$2} /MemAvailable/ {avail=$2} END { + if (total > 0) { + used=(total-avail)/1024/1024 + total_gb=total/1024/1024 + printf "%.1fG/%.0fG", used, total_gb + } else { + print "N/A" + } + }' /proc/meminfo 2>/dev/null || echo "N/A" +} + +# Disk usage (single df call) +_motd_disk() { + df -h / 2>/dev/null | awk 'NR==2 {print $3 "/" $2}' || echo "N/A" +} + +# CPU governor (Arch-specific, direct file read) +_motd_governor() { + local gov=$(cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor 2>/dev/null) + [[ -n "$gov" ]] && echo "$gov" +} + +# Kernel version (simplified) +_motd_kernel() { + local kernel=$(uname -r) + # Strip architecture suffix for cleaner display + echo "${kernel%%-*}" +} + +# CachyOS scheduler detection +_motd_scheduler() { + if grep -q "cachyos" /proc/version 2>/dev/null; then + if grep -q "bore" /proc/version 2>/dev/null; then + echo "BORE" + elif grep -q "eevdf" /proc/version 2>/dev/null; then + echo "EEVDF" + else + echo "CachyOS" + fi fi } -_motd_mem() { - free -h 2>/dev/null | awk '/^Mem:/ {print $3 "/" $2}' || echo "N/A" +# Failed systemd services count (cached) +_motd_failed_services() { + # Use cache to avoid slow systemctl calls on every prompt + local cache_file="/tmp/.motd-failed-${UID}" + local cache_age=300 # 5 minutes + + if [[ -f "$cache_file" ]]; then + local file_age=$(($(date +%s) - $(stat -c %Y "$cache_file" 2>/dev/null || echo 0))) + if (( file_age < cache_age )); then + cat "$cache_file" + return + fi + fi + + local count=$(systemctl --failed --no-pager --no-legend 2>/dev/null | wc -l) + echo "$count" > "$cache_file" 2>/dev/null + echo "$count" } -_motd_disk() { - df -h / 2>/dev/null | awk 'NR==2 {print $3 "/" $4}' || echo "N/A" +# Package updates (from environment, set by aliases.zsh) +_motd_updates() { + echo "${UPDATE_PKG_COUNT:-0}" } # ============================================================================ @@ -62,27 +130,13 @@ _motd_disk() { _motd_line() { local char="$1" - local i local line="" - for ((i=0; i<_M_WIDTH; i++)); do - line+="$char" - done + for ((i=0; i<_M_WIDTH; i++)); do line+="$char"; done echo "$line" } -_motd_pad() { - local str="$1" - local width="$2" - local len=${#str} - if (( len >= width )); then - echo "${str:0:$width}" - else - printf "%-${width}s" "$str" - fi -} - # ============================================================================ -# Main Display Function +# Main Display Function (Compact) # ============================================================================ show_motd() { @@ -95,7 +149,6 @@ show_motd() { local load=$(_motd_load) local mem=$(_motd_mem) local disk=$(_motd_disk) - local local_ip=$(hostname -i 2>/dev/null | awk -F" " '{print $1}' || echo "N/A") local hline=$(_motd_line '═') local inner=$((_M_WIDTH - 2)) @@ -106,22 +159,38 @@ show_motd() { # Header: hostname + datetime local h_left="✦ ${hostname}" - local h_center="${local_ip}" local h_right="${datetime}" - local h_pad=$(((inner - ${#h_left} - ${#h_center} - ${#h_right}) / 2 )) + local h_pad=$(((inner - ${#h_left} - ${#h_right}) / 2)) local h_spaces="" for ((i=0; i 0 )); then + alerts+="${DF_RED}⚠ ${failed} failed service(s)${DF_NC} " + fi + + # Check for updates + local updates=$(_motd_updates) + if (( updates > 0 )); then + alerts+="${DF_YELLOW}⇑ ${updates} update(s)${DF_NC}" + fi + + [[ -n "$alerts" ]] && echo " $alerts" echo "" } @@ -137,8 +206,64 @@ show_motd_mini() { local hostname="${HOST:-$(hostname -s 2>/dev/null)}" local uptime=$(_motd_uptime) local mem=$(_motd_mem) + local failed=$(_motd_failed_services) + + local alert="" + (( failed > 0 )) && alert=" ${DF_RED}[${failed} failed]${DF_NC}" - echo "${DF_DIM}──${DF_NC} ${DF_BOLD}${hostname}${DF_NC} ${DF_DIM}│${DF_NC} up:${uptime} ${DF_DIM}│${DF_NC} mem:${mem} ${DF_DIM}──${DF_NC}" + echo "${DF_DIM}──${DF_NC} ${DF_BOLD}${hostname}${DF_NC} ${DF_DIM}│${DF_NC} up:${uptime} ${DF_DIM}│${DF_NC} mem:${mem}${alert} ${DF_DIM}──${DF_NC}" +} + +# ============================================================================ +# Full Format (Extended Info) +# ============================================================================ + +show_motd_full() { + [[ -n "$_MOTD_SHOWN" && "$1" != "--force" ]] && return 0 + typeset -g _MOTD_SHOWN=1 + + local hostname="${HOST:-$(hostname -s 2>/dev/null)}" + local datetime=$(date '+%A, %B %d %Y %H:%M:%S') + local uptime=$(_motd_uptime) + local load=$(_motd_load) + local mem=$(_motd_mem) + local disk=$(_motd_disk) + local kernel=$(_motd_kernel) + local governor=$(_motd_governor) + local scheduler=$(_motd_scheduler) + local hline=$(_motd_line '═') + + echo "" + echo "${DF_GREY}╒${hline}╕${DF_NC}" + echo "${DF_GREY}│${DF_NC} ${DF_BOLD}${DF_BLUE}✦ ${hostname}${DF_NC}" + echo "${DF_GREY}│${DF_NC} ${DF_DIM}${datetime}${DF_NC}" + echo "${DF_GREY}├$(_motd_line '─')┤${DF_NC}" + + # System info + echo "${DF_GREY}│${DF_NC} ${DF_CYAN}Kernel:${DF_NC} ${kernel}" + [[ -n "$scheduler" ]] && echo "${DF_GREY}│${DF_NC} ${DF_CYAN}Scheduler:${DF_NC} ${scheduler}" + [[ -n "$governor" ]] && echo "${DF_GREY}│${DF_NC} ${DF_CYAN}Governor:${DF_NC} ${governor}" + + echo "${DF_GREY}├$(_motd_line '─')┤${DF_NC}" + + # Resources + echo "${DF_GREY}│${DF_NC} ${DF_YELLOW}▲ Uptime:${DF_NC} ${uptime}" + echo "${DF_GREY}│${DF_NC} ${DF_CYAN}◆ Load:${DF_NC} ${load}" + echo "${DF_GREY}│${DF_NC} ${DF_GREEN}◇ Memory:${DF_NC} ${mem}" + echo "${DF_GREY}│${DF_NC} ${DF_BLUE}⊡ Disk:${DF_NC} ${disk}" + + # Alerts section + local failed=$(_motd_failed_services) + local updates=$(_motd_updates) + + if (( failed > 0 || updates > 0 )); then + echo "${DF_GREY}├$(_motd_line '─')┤${DF_NC}" + (( failed > 0 )) && echo "${DF_GREY}│${DF_NC} ${DF_RED}⚠ ${failed} failed systemd service(s)${DF_NC}" + (( updates > 0 )) && echo "${DF_GREY}│${DF_NC} ${DF_YELLOW}⇑ ${updates} package update(s) available${DF_NC}" + fi + + echo "${DF_GREY}╘${hline}╛${DF_NC}" + echo "" } # ============================================================================ @@ -147,3 +272,29 @@ show_motd_mini() { alias motd='show_motd --force' alias motd-mini='show_motd_mini --force' +alias motd-full='show_motd_full --force' + +# ============================================================================ +# Quick System Overview (callable anytime) +# ============================================================================ + +sysbrief() { + echo -e "${DF_CYAN}Uptime:${DF_NC} $(_motd_uptime)" + echo -e "${DF_CYAN}Load:${DF_NC} $(_motd_load)" + echo -e "${DF_CYAN}Memory:${DF_NC} $(_motd_mem)" + echo -e "${DF_CYAN}Disk:${DF_NC} $(_motd_disk)" + + local kernel=$(_motd_kernel) + echo -e "${DF_CYAN}Kernel:${DF_NC} ${kernel}" + + local governor=$(_motd_governor) + [[ -n "$governor" ]] && echo -e "${DF_CYAN}Governor:${DF_NC} ${governor}" + + local scheduler=$(_motd_scheduler) + [[ -n "$scheduler" ]] && echo -e "${DF_CYAN}Scheduler:${DF_NC} ${scheduler}" + + local failed=$(_motd_failed_services) + if (( failed > 0 )); then + echo -e "${DF_RED}Failed:${DF_NC} ${failed} service(s)" + fi +} diff --git a/zsh/functions/systemd-helpers.zsh b/zsh/functions/systemd-helpers.zsh new file mode 100644 index 0000000..83af163 --- /dev/null +++ b/zsh/functions/systemd-helpers.zsh @@ -0,0 +1,348 @@ +# ============================================================================ +# 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' +} + +# ============================================================================ +# Core Systemctl Shortcuts +# ============================================================================ + +# 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 +} + +# 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 +} + +# 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 +} + +# 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" +} + +# Show recent logs for a service (without follow) +sclogs() { + local service="$1" + local lines="${2:-50}" + [[ -z "$service" ]] && { echo "Usage: sclog [lines]"; return 1; } + + sudo journalctl -xeu "$service" -n "$lines" --no-pager +} + +# ============================================================================ +# Service Status Commands +# ============================================================================ + +# Show failed services (system and user) +sc-failed() { + echo -e "${DF_BLUE}╔════════════════════════════════════════════════════════════╗${DF_NC}" + echo -e "${DF_BLUE}║${DF_NC} Failed Services ${DF_BLUE}║${DF_NC}" + echo -e "${DF_BLUE}╚════════════════════════════════════════════════════════════╝${DF_NC}" + + echo -e "\n${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 + + echo "" +} + +# Show active timers +sc-timers() { + echo -e "${DF_BLUE}╔════════════════════════════════════════════════════════════╗${DF_NC}" + echo -e "${DF_BLUE}║${DF_NC} Active Timers ${DF_BLUE}║${DF_NC}" + echo -e "${DF_BLUE}╚════════════════════════════════════════════════════════════╝${DF_NC}" + + echo -e "\n${DF_CYAN}System Timers:${DF_NC}" + systemctl list-timers --no-pager | head -20 + + echo -e "\n${DF_CYAN}User Timers:${DF_NC}" + systemctl --user list-timers --no-pager 2>/dev/null | head -10 + + echo "" +} + +# Show recently started/stopped services +sc-recent() { + local count="${1:-15}" + + echo -e "${DF_BLUE}╔════════════════════════════════════════════════════════════╗${DF_NC}" + echo -e "${DF_BLUE}║${DF_NC} Recent Service Activity ${DF_BLUE}║${DF_NC}" + echo -e "${DF_BLUE}╚════════════════════════════════════════════════════════════╝${DF_NC}" + + echo -e "\n${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() { + echo -e "${DF_BLUE}╔════════════════════════════════════════════════════════════╗${DF_NC}" + echo -e "${DF_BLUE}║${DF_NC} Boot Time Analysis ${DF_BLUE}║${DF_NC}" + echo -e "${DF_BLUE}╚════════════════════════════════════════════════════════════╝${DF_NC}" + + echo -e "\n${DF_CYAN}Boot Summary:${DF_NC}" + systemd-analyze + + echo -e "\n${DF_CYAN}Slowest Services (top 10):${DF_NC}" + 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; } + + echo -e "${DF_BLUE}==>${DF_NC} Searching for services matching: ${query}" + echo "" + + systemctl list-unit-files --type=service --no-pager | grep -i "$query" +} + +# Show detailed service info +sc-info() { + local service="$1" + [[ -z "$service" ]] && { echo "Usage: sc-info "; return 1; } + + echo -e "${DF_BLUE}╔════════════════════════════════════════════════════════════╗${DF_NC}" + echo -e "${DF_BLUE}║${DF_NC} Service Info: ${service}" + echo -e "${DF_BLUE}╚════════════════════════════════════════════════════════════╝${DF_NC}" + + echo -e "\n${DF_CYAN}Status:${DF_NC}" + systemctl status "$service" --no-pager -l 2>/dev/null || \ + sudo systemctl status "$service" --no-pager -l + + echo -e "\n${DF_CYAN}Unit File:${DF_NC}" + systemctl cat "$service" 2>/dev/null | head -30 + + echo "" +} + +# ============================================================================ +# 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 + 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" + } +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() { + echo -e "${DF_BLUE}╔════════════════════════════════════════════════════════════╗${DF_NC}" + echo -e "${DF_BLUE}║${DF_NC} Systemd Helper Commands ${DF_BLUE}║${DF_NC}" + echo -e "${DF_BLUE}╚════════════════════════════════════════════════════════════╝${DF_NC}" + + 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) + scogs 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 + +EOF +}