FocusedWolf

Arch: pacman+yay scripts to update like a boss

Feb 26th, 2024 (edited)
1,978
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Bash 48.06 KB | None | 0 0
  1. File: /usr/local/bin/yar
  2. USAGE: $ yar    <-- Update the mirrors list.
  3.  
  4.     #!/usr/bin/env python3
  5.  
  6.     # Installation:
  7.     #     $ sudo cp ./yar /usr/local/bin/
  8.     #     $ sudo chmod +x /usr/local/bin/yar
  9.  
  10.     # ----- Colors -----
  11.     BLACK = '\033[30m'
  12.     RED = '\033[31m'
  13.     GREEN = '\033[32m'
  14.     YELLOW = '\033[33m'
  15.     BLUE = '\033[34m'
  16.     MAGENTA = '\033[35m'
  17.     CYAN = '\033[36m'
  18.     WHITE = '\033[37m'
  19.     BRIGHT_BLACK = '\033[90m'
  20.     BRIGHT_RED = '\033[91m'
  21.     BRIGHT_GREEN = '\033[92m'
  22.     BRIGHT_YELLOW = '\033[93m'
  23.     BRIGHT_BLUE = '\033[94m'
  24.     BRIGHT_MAGENTA = '\033[95m'
  25.     BRIGHT_CYAN = '\033[96m'
  26.     BRIGHT_WHITE = '\033[97m'
  27.  
  28.     # ----- Styles -----
  29.     RESET = '\033[0m' # Reset all styles.
  30.     BOLD = '\033[1m'
  31.     HALF_BRIGHT = '\033[2m'
  32.     ITALIC = '\033[3m'
  33.     UNDERLINE = '\033[4m'
  34.     REVERSED = '\033[7m' # Reverse foreground and background colors.
  35.  
  36.     import math
  37.     import requests
  38.     import shutil
  39.     import subprocess
  40.     import sys
  41.  
  42.     MIRRORLIST_PATH = '/etc/pacman.d/mirrorlist'
  43.     EARTH_RADIUS_KM = 6371.0
  44.  
  45.     # Reference point: New York City
  46.     USER_LAT = 40.730610
  47.     USER_LON = -73.935242
  48.  
  49.     # Generated country coordinates (lat, lon).
  50.     COUNTRY_COORDS = {
  51.         'Armenia': (40.0, 45.0),
  52.         'Australia': (-27.0, 133.0),
  53.         'Austria': (47.33333333, 13.33333333),
  54.         'Azerbaijan': (40.5, 47.5),
  55.         'Bangladesh': (24.0, 90.0),
  56.         'Belarus': (53.0, 28.0),
  57.         'Belgium': (50.83333333, 4.0),
  58.         'Brazil': (-10.0, -55.0),
  59.         'Bulgaria': (43.0, 25.0),
  60.         'Cambodia': (13.0, 105.0),
  61.         'Canada': (60.0, -95.0),
  62.         'Chile': (-30.0, -71.0),
  63.         'China': (35.0, 105.0),
  64.         'Colombia': (4.0, -72.0),
  65.         'Croatia': (45.16666666, 15.5),
  66.         'Czechia': (49.75, 15.5),
  67.         'Denmark': (56.0, 10.0),
  68.         'Ecuador': (-2.0, -77.5),
  69.         'Estonia': (59.0, 26.0),
  70.         'Finland': (64.0, 26.0),
  71.         'France': (46.0, 2.0),
  72.         'Georgia': (42.0, 43.5),
  73.         'Germany': (51.0, 9.0),
  74.         'Greece': (39.0, 22.0),
  75.         'Hong Kong': (22.267, 114.188),
  76.         'Hungary': (47.0, 20.0),
  77.         'Iceland': (65.0, -18.0),
  78.         'India': (20.0, 77.0),
  79.         'Indonesia': (-5.0, 120.0),
  80.         'Iran': (32.0, 53.0),
  81.         'Israel': (31.47, 35.13),
  82.         'Italy': (42.83333333, 12.83333333),
  83.         'Japan': (36.0, 138.0),
  84.         'Kazakhstan': (48.0196, 66.9237),
  85.         'Kenya': (1.0, 38.0),
  86.         'Latvia': (57.0, 25.0),
  87.         'Lithuania': (56.0, 24.0),
  88.         'Luxembourg': (49.75, 6.16666666),
  89.         'Mauritius': (-20.28333333, 57.55),
  90.         'Mexico': (23.0, -102.0),
  91.         'Moldova': (47.0, 29.0),
  92.         'Morocco': (32.0, -5.0),
  93.         'Nepal': (28.0, 84.0),
  94.         'Netherlands': (52.5, 5.75),
  95.         'New Caledonia': (-21.5, 165.5),
  96.         'New Zealand': (-41.0, 174.0),
  97.         'North Macedonia': (41.83333333, 22.0),
  98.         'Norway': (62.0, 10.0),
  99.         'Paraguay': (-23.0, -58.0),
  100.         'Poland': (52.0, 20.0),
  101.         'Portugal': (39.5, -8.0),
  102.         'Romania': (46.0, 25.0),
  103.         'Russia': (60.0, 100.0),
  104.         'Réunion': (-21.15, 55.5),
  105.         'Saudi Arabia': (25.0, 45.0),
  106.         'Serbia': (44.0, 21.0),
  107.         'Singapore': (1.36666666, 103.8),
  108.         'Slovakia': (48.66666666, 19.5),
  109.         'Slovenia': (46.11666666, 14.81666666),
  110.         'South Africa': (-29.0, 24.0),
  111.         'South Korea': (37.0, 127.5),
  112.         'Spain': (40.0, -4.0),
  113.         'Sweden': (62.0, 15.0),
  114.         'Switzerland': (47.0, 8.0),
  115.         'Taiwan': (23.5, 121.0),
  116.         'Thailand': (15.0, 100.0),
  117.         'Türkiye': (39.0, 35.0),
  118.         'Ukraine': (49.0, 32.0),
  119.         'United Arab Emirates': (24.0, 54.0),
  120.         'United Kingdom': (54.0, -2.0),
  121.         'United States': (38.0, -97.0),
  122.         'Uzbekistan': (41.0, 64.0),
  123.         'Vietnam': (16.16666666, 107.83333333),
  124.     }
  125.  
  126.     # Calculate the great-circle distance between two points on a sphere using the Haversine formula.
  127.     def haversine_distance(lat1, lon1, lat2, lon2, radius):
  128.         # Convert latitude and longitude from degrees to radians.
  129.         lat1 = math.radians(lat1)
  130.         lon1 = math.radians(lon1)
  131.         lat2 = math.radians(lat2)
  132.         lon2 = math.radians(lon2)
  133.  
  134.         # Calculate the differences.
  135.         dlat = lat2 - lat1
  136.         dlon = lon2 - lon1
  137.  
  138.         # Haversine formula.
  139.         # SOURCE: https://www.movable-type.co.uk/scripts/latlong.html
  140.         # The square of half the chord length between the points.
  141.         a = math.sin(dlat / 2)**2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon / 2)**2
  142.         # The angular distance in radians.
  143.         c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
  144.         d = radius * c
  145.         return d
  146.  
  147.     def generate_sorted_countries_list(lat, lon, max_countries=5):
  148.         # Generate a list of nearest countries.
  149.         distances = []
  150.         for country, (country_lat, country_lon) in COUNTRY_COORDS.items():
  151.             dist = haversine_distance(lat, lon, country_lat, country_lon, EARTH_RADIUS_KM)
  152.             distances.append((dist, country))
  153.  
  154.         distances.sort(key=lambda x: x[0])
  155.         nearest = [country for _, country in distances[:max_countries]]
  156.         return ','.join(nearest)
  157.  
  158.     def update_arch_mirrors():
  159.         # Run reflector with closest countries.
  160.         if shutil.which('reflector') is None:
  161.             print(f'{BRIGHT_RED}::{RESET} Reflector could not be found. Install with:')
  162.             print('   sudo pacman -S reflector')
  163.             sys.exit(1)
  164.  
  165.         print(f'{BRIGHT_BLUE}::{RESET} Updating pacman mirrors . . .')
  166.  
  167.         countries = generate_sorted_countries_list(USER_LAT, USER_LON, 8)
  168.         print(f'Nearby Countries: {countries}')
  169.  
  170.         cmd = [
  171.             'sudo', 'reflector',
  172.             '--verbose',
  173.             '--country', countries,
  174.             '--protocol', 'https',
  175.             '--sort', 'rate',
  176.             '--age', '1',
  177.             '--save', MIRRORLIST_PATH,
  178.         ]
  179.  
  180.         try:
  181.             subprocess.run(cmd, check=True)
  182.             print(f'{BRIGHT_GREEN}:: Mirrorlist {MIRRORLIST_PATH} updated successfully . . .{RESET}')
  183.         except subprocess.CalledProcessError:
  184.             print(f'{BRIGHT_RED}:: Failed to update mirrorlist {MIRRORLIST_PATH} . . .{RESET}')
  185.             sys.exit(1)
  186.  
  187.     def generate_country_coords():
  188.         # Ask reflector for supported countries.
  189.         result = subprocess.run(
  190.             ['reflector', '--list-countries'],
  191.             capture_output=True, text=True, check=True
  192.         )
  193.         reflector_lines = result.stdout.splitlines()[3:] # Skip headers.
  194.         valid_countries = {
  195.             ' '.join(line.split()[:-2]) for line in reflector_lines if line.strip()
  196.         }
  197.  
  198.         # Query RestCountries API for lat/lon.
  199.         url = 'https://restcountries.com/v3.1/all?fields=name,latlng'
  200.         resp = requests.get(url)
  201.         resp.raise_for_status()
  202.         data = resp.json()
  203.  
  204.         # Map country names to match reflector spelling.
  205.         NAME_MAP = {
  206.             'Turkey': 'Türkiye',
  207.         }
  208.  
  209.         # Print as Python dict.
  210.         print('COUNTRY_COORDS = {')
  211.         for country in sorted(data, key=lambda c: c.get('name', {}).get('common', '')):
  212.             name = country.get('name', {}).get('common')
  213.             latlng = country.get('latlng')
  214.             if not (name and latlng):
  215.                 continue
  216.             reflector_name = NAME_MAP.get(name, name)
  217.             if reflector_name in valid_countries:
  218.                 lat, lon = latlng[0], latlng[1]
  219.                 print(f"    '{reflector_name}': ({lat}, {lon}),")
  220.         print('}')
  221.  
  222.     if __name__ == '__main__':
  223.         # Uncomment to generate COUNTRY_COORDS dictionary.
  224.         # generate_country_coords()
  225.         # sys.exit(0)
  226.  
  227.         update_arch_mirrors()
  228.  
  229. File: /usr/local/bin/yip
  230. USAGE: $ yip    <-- Update installed packages, delete downloaded packages, purge the orphans, and check if the system can boot, like a boss.
  231.  
  232.     #!/bin/bash
  233.  
  234.     # Installation:
  235.     #     $ sudo cp ./yip /usr/local/bin/
  236.     #     $ sudo chmod +x /usr/local/bin/yip
  237.  
  238.     # ----- Colors -----
  239.     BLACK="\033[30m"
  240.     RED="\033[31m"
  241.     GREEN="\033[32m"
  242.     YELLOW="\033[33m"
  243.     BLUE="\033[34m"
  244.     MAGENTA="\033[35m"
  245.     CYAN="\033[36m"
  246.     WHITE="\033[37m"
  247.     BRIGHT_BLACK="\033[90m"
  248.     BRIGHT_RED="\033[91m"
  249.     BRIGHT_GREEN="\033[92m"
  250.     BRIGHT_YELLOW="\033[93m"
  251.     BRIGHT_BLUE="\033[94m"
  252.     BRIGHT_MAGENTA="\033[95m"
  253.     BRIGHT_CYAN="\033[96m"
  254.     BRIGHT_WHITE="\033[97m"
  255.  
  256.     # ----- Styles -----
  257.     RESET="\033[0m" # Reset all styles.
  258.     BOLD="\033[1m"
  259.     HALF_BRIGHT="\033[2m"
  260.     ITALIC="\033[3m"
  261.     UNDERLINE="\033[4m"
  262.     REVERSED="\033[7m" # Reverse foreground/background.
  263.  
  264.     # Prompt for sudo at the start.
  265.     if ! sudo -v; then
  266.         echo -e "${BRIGHT_BLUE}::${RESET} This script requires sudo privileges. Exiting."
  267.         exit 1
  268.     fi
  269.  
  270.     # Get the directory containing this script.
  271.     SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  272.  
  273.    tty_check() {
  274.        # If running in a graphical session.
  275.        if [[ -n $DISPLAY ]]; then
  276.            while true; do
  277.                echo
  278.                echo -e "${CYAN}:: WARNING: Close ALL programs before continuing. Ideally you should boot to a TTY session before running this script.${RESET}"
  279.                echo -e "${CYAN}            Having programs running during an update can cause errors due to old libraries still being in use.${RESET}"
  280.                echo
  281.                read -p "Do you want to continue? [Y/N]: " response
  282.                # Set response to 'Y' if you didn't press Y|N so Enter allows you to continue like with Yay/Pacman.
  283.                response=${response:-Y}
  284.                case "$response" in
  285.                    y|Y) break ;;
  286.                    n|N) exit 1 ;; # Exit script with status 1 to indicate failure.
  287.                    *) echo; echo "Please answer Y or N." ;;
  288.                esac
  289.            done
  290.        fi
  291.    }
  292.    tty_check
  293.  
  294.    check_for_update_news() {
  295.        echo -e "${BRIGHT_BLUE}::${RESET} Checking Arch Linux RSS feed . . ."
  296.  
  297.        "$SCRIPT_DIR/yap" --current
  298.        status=$?
  299.  
  300.        case $status in
  301.            2)  # News detected.
  302.                echo -e "${BRIGHT_CYAN}:: WARNING: News event detected.${RESET}"
  303.                ;;
  304.            1)  # Error detected.
  305.                # No need for a error message because the called script will display its own.
  306.                # echo -e "${BRIGHT_RED}:: ERROR: Could not fetch Arch Linux news!${RESET}
  307.                 ;;
  308.             0)  # No news or error detected.
  309.                 return
  310.                 ;;
  311.             *)  # Unexpected exit code.
  312.                 echo -e "${BRIGHT_RED}:: Unknown error occurred (exit code $status)!${RESET}"
  313.                 ;;
  314.         esac
  315.  
  316.         while true; do
  317.             echo
  318.             read -p "Do you want to continue? [Y/N]: " response
  319.             response=${response:-Y}
  320.             case "$response" in
  321.                 y|Y) break ;;
  322.                 n|N) exit 1 ;; # Exit script with status 1 to indicate failure.
  323.                 *) echo; echo "Please answer Y or N." ;;
  324.             esac
  325.         done
  326.     }
  327.     check_for_update_news
  328.  
  329.     free_space_check() {
  330.         local directory=$1
  331.         local low_space_mb=$2
  332.  
  333.         # If directory does not exist or is not a directory.
  334.         if [[ ! -d "$directory" ]]; then
  335.             echo
  336.             echo -e "${RED}::${RESET} ERROR: Directory '$directory' does not exist."
  337.             exit 1 # Exit script with status 1 to indicate failure.
  338.         fi
  339.  
  340.         # Get all space values in bytes.
  341.         local low_space_bytes=$(awk "BEGIN {print $low_space_mb * 1024 * 1024}") # Using awk because bash will overflow: $ echo $((9999999999999 * 1024 * 1024)) # will display -7960984073710600192.
  342.         local free_space_bytes=$(df --block-size=1 --output=avail "$directory" 2>/dev/null | tail -n1)
  343.         local total_space_bytes=$(df --block-size=1 --output=size "$directory" 2>/dev/null | tail -n1)
  344.  
  345.         # If free space is less than low space.
  346.         if awk -v free="$free_space_bytes" -v low="$low_space_bytes" 'BEGIN { exit !(free < low) }'; then
  347.             local si_scalars=(1208925819614629174706176 1180591620717411303424 1152921504606846976 1125899906842624 1099511627776 1073741824 1048576 1024)
  348.             local si_prefixes=("YiB" "ZiB" "EiB" "PiB" "TiB" "GiB" "MiB" "KiB")
  349.  
  350.             # Find the appropriate SI prefix.
  351.             local scalar=1
  352.             local prefix="KiB"
  353.             for i in ${!si_scalars[@]}; do
  354.                 if awk -v space="$total_space_bytes" -v scalar="${si_scalars[$i]}" 'BEGIN { exit !(space >= scalar) }'; then
  355.                     scalar="${si_scalars[$i]}"
  356.                     prefix="${si_prefixes[$i]}"
  357.                     break
  358.                 fi
  359.             done
  360.  
  361.             # Calculate output space info.
  362.             local free_space_scaled=$(awk -v bytes="$free_space_bytes" -v scalar="$scalar" 'BEGIN { printf "%.1f", bytes / scalar }')
  363.             local total_space_scaled=$(awk -v bytes="$total_space_bytes" -v scalar="$scalar" 'BEGIN { printf "%.1f", bytes / scalar }')
  364.             local used_percent=$(awk -v free="$free_space_bytes" -v total="$total_space_bytes" 'BEGIN { printf "%.f", (1 - free / total) * 100 }')
  365.  
  366.             while true; do
  367.                 # Displays disk space info like Dolphin properties window.
  368.                 echo
  369.                 echo -e "${RED}:: WARNING: Low disk space detected. ${free_space_scaled} ${prefix} free of ${total_space_scaled} ${prefix} (${used_percent}% used) on drive '${directory}'.${RESET}"
  370.                 echo -e "${RED}            Running out of room during an update can cause incomplete installations, system crashes, or render the system unbootable.${RESET}"
  371.                 echo
  372.                 read -p "Do you want to continue? [Y/N]: " response
  373.                 # Set response to 'Y' if you didn't press Y|N so Enter allows you to continue like with Yay/Pacman.
  374.                 response=${response:-Y}
  375.                 case "$response" in
  376.                     y|Y) break ;;
  377.                     n|N) exit 1 ;; # Exit script with status 1 to indicate failure.
  378.                     *) echo; echo "Please answer Y or N." ;;
  379.                 esac
  380.             done
  381.         fi
  382.     }
  383.     # WIP low-space checks. Feel free to mod.
  384.     free_space_check /efi      50    # If you use /boot/efi then change "/efi" to "/boot/efi".
  385.     free_space_check /boot     500
  386.     free_space_check /         10240
  387.     free_space_check "$HOME"   5120    # Probably not needed for updates but important to use computer.
  388.  
  389.     is_grub_installed() {
  390.         command -v grub-install &>/dev/null || find /boot /efi /EFI -type d -iname "grub" -quit &>/dev/null
  391.     }
  392.  
  393.     is_refind_installed() {
  394.         command -v refind-install &>/dev/null || find /boot /efi /EFI -type d -iname "refind" -quit &>/dev/null
  395.     }
  396.  
  397.     is_systemd_boot_installed() {
  398.         # If the bootctl command does not exist return false (1).
  399.         # This command is very likely to exist since all systems that installed the "base" arch package have systemd which installs bootctl.
  400.         command -v bootctl &>/dev/null || return 1
  401.  
  402.         # Ask bootctl if systemd-boot is installed, otherwise look for it.
  403.         sudo bootctl is-installed &>/dev/null || find /boot /efi /EFI -type d -iname "loader" -quit &>/dev/null
  404.     }
  405.  
  406.     is_pacman_hanging() {
  407.         local wait_count=0
  408.         local wait_time=7
  409.         while pgrep -x "pacman" >/dev/null || [ -f /var/lib/pacman/db.lck ]; do
  410.             case $wait_count in
  411.                 0)
  412.                     # First time don't print any warning.
  413.                     ;;
  414.                 1)
  415.                     echo
  416.                     echo -e "${RED}:: WARNING: Pacman is still running or its lock is still active. Waiting . . .${RESET}"
  417.                     wait_time=$((wait_time * 2))
  418.                     ;;
  419.                 2)
  420.                     wait_time=1
  421.                     echo
  422.                     echo -e "${RED}Pacman seems to be misbehaving. Waiting . . .${RESET}"
  423.                     echo
  424.                     echo -e "${RED}If you restart now you might find an unbootable machine due to missing or outdated initramfs and kernel modules.${RESET}"
  425.                     echo
  426.                     echo -e "${RED}Here are some steps that might help:${RESET}"
  427.                     echo
  428.                     echo -e "${RED}1. Kill the running pacman process and remove the lock:${RESET}"
  429.                     echo -e "${RED}   $ sudo killall pacman${RESET}"
  430.                     echo -e "${RED}   $ sudo rm /var/lib/pacman/db.lck${RESET}"
  431.                     echo -e "${RED}   This is necessary to run pacman again.${RESET}"
  432.                     echo
  433.                     echo -e "${RED}2. Reinstall critical packages:${RESET}"
  434.                     echo -e "${RED}   $ sudo pacman -S udev mkinitcpio linux${RESET}"
  435.                     echo -e "${RED}   $ sudo mkinitcpio -P${RESET}"
  436.                     echo -e "${RED}   This will reinstall essential packages and ensure the kernel and initramfs are created.${RESET}"
  437.                     echo
  438.                     echo -e "${RED}3. Verify package integrity:${RESET}"
  439.                     echo -e "${RED}   $ sudo pacman -Qkk${RESET}"
  440.                     echo -e "${RED}   This will check for any missing files or integrity issues with installed packages.${RESET}"
  441.                     echo
  442.                     if is_grub_installed; then
  443.                         echo -e "${RED}4. Regenerate the GRUB configuration:${RESET}"
  444.                         echo -e "${RED}   $ sudo grub-mkconfig -o /boot/grub/grub.cfg${RESET}"
  445.                     elif is_refind_installed; then
  446.                         echo -e "${RED}4. Regenerate the rEFInd configuration:${RESET}"
  447.                         echo -e "${RED}   $ sudo refind-install${RESET}"
  448.                     elif is_systemd_boot_installed; then
  449.                         echo -e "${RED}4. Regenerate the systemd-boot configuration:${RESET}"
  450.                         echo -e "${RED}   $ sudo bootctl update${RESET}"
  451.                     else
  452.                         echo -e "${RED}4. Reinstall your boot loader.${RESET}"
  453.                     fi
  454.                     echo
  455.                     echo -e "${RED}5. After performing these steps try running this script again.${RESET}"
  456.                     echo
  457.                     echo -e "${RED}Press Ctrl+C to stop waiting for pacman and exit this script.${RESET}"
  458.                     ;;
  459.             esac
  460.             sleep $wait_time
  461.             ((wait_count++))
  462.         done
  463.         if ((wait_count > 1)); then
  464.             echo -e "${GREEN}:: Pacman seems to have recovered. Continuing with the script . . .${RESET}"
  465.         fi
  466.     }
  467.  
  468.     update_arch() {
  469.         # Refresh package databases and ensure keyring is up-to-date.
  470.         # SOURCE: https://www.reddit.com/r/archlinux/comments/1leg9ds/why_doesnt_pacman_just_install_archlinuxkeyring/
  471.         is_pacman_hanging
  472.         sudo pacman -Sy --noconfirm --needed archlinux-keyring
  473.         # sudo pacman -Syy --noconfirm --needed archlinux-keyring # Uses -Syy to force-refresh package databases.
  474.  
  475.         # Update packages including AUR.
  476.         is_pacman_hanging
  477.         yay -Syu
  478.         # yay -Syu --noconfirm # Update everything without question.
  479.  
  480.         echo
  481.         echo -e "${BRIGHT_BLUE}::${RESET} Searching for *.pacnew, *.pacorig, and *.pacsave files . . ."
  482.         sudo find /etc /usr -type f \( -iname '*.pacnew' -o -iname '*.pacorig' -o -iname '*.pacsave' \) | grep --color=always -E '.*'
  483.     }
  484.     update_arch
  485.  
  486.     remove_orphaned_packages() {
  487.         # Remove unneeded dependencies.
  488.         # NOTE: Strangely both [$ yes | yay -Scc] and [$ yay -Scc --noconfirm] will prompt you to remove dependencies so [yay -Ycc --noconfirm] needs to be called first to prevent that prompt from displaying.
  489.         is_pacman_hanging
  490.         echo
  491.         echo -e "${BRIGHT_BLUE}::${RESET} Remove unneeded dependencies . . ."
  492.         yay -Ycc --noconfirm
  493.  
  494.         # Clean Pacman cache directory /var/cache/pacman/pkg/, Pacman database directory /var/lib/pacman/, and Yay AUR build directory $HOME/.cache/yay/.
  495.         # If you need to downgrade or install an outdated package then you will need to download it again.
  496.         # NOTE: To just clean the Pacman directories the command is [$ yes | sudo pacman -Scc].
  497.         is_pacman_hanging
  498.         echo
  499.         echo -e "${BRIGHT_BLUE}::${RESET} Clean Pacman and Yay caches . . ."
  500.         yes | yay -Scc
  501.  
  502.         # Purge the orphans.
  503.         is_pacman_hanging
  504.         mapfile -t orphans < <(pacman -Qdtq)
  505.         if [[ ${#orphans[@]} -gt 0 ]]; then
  506.             echo
  507.             echo -e "${BRIGHT_BLUE}::${RESET} Removing orphaned packages . . ."
  508.             # sudo pacman -Rns "${orphans[@]}"
  509.             sudo pacman -Rns --noconfirm "${orphans[@]}"
  510.         else
  511.             echo
  512.             echo -e "${BRIGHT_BLUE}::${RESET} No orphaned packages found . . ."
  513.         fi
  514.     }
  515.     remove_orphaned_packages
  516.  
  517.     remove_orphaned_kernel_modules() {
  518.         # Build a map of installed kernel versions based on current /boot images.
  519.         declare -A kernel_versions
  520.         for img in /boot/vmlinuz-*; do
  521.             [[ -f "$img" ]] || continue
  522.             pkgname="${img#/boot/vmlinuz-}" # Strip '/boot/vmlinuz-' to get the kernel package name e.g. 'linux', 'linux-lts', 'linux-hardened', 'linux-zen' etc.
  523.  
  524.             # If a package named $pkgname is installed.
  525.             if pacman -Q "$pkgname" &>/dev/null; then
  526.                 # Add an associative array entry to map a kernel package to its installed version.
  527.                 kernel_versions["$pkgname"]="$(pacman -Q "$pkgname" | awk '{print $2}')"
  528.             fi
  529.         done
  530.  
  531.         # Collect orphaned module directories.
  532.         local orphans=()
  533.         for dir in /usr/lib/modules/*/; do # The trailing '/' ensures only directories are matched.
  534.             [[ -d "$dir" ]] || continue # Skip if this isn't a directory.
  535.             module_dir=$(basename "$dir")
  536.  
  537.             skip=false
  538.             for version in "${kernel_versions[@]}"; do
  539.                 [[ "$module_dir" == "$version" ]] && { skip=true; break; }
  540.             done
  541.             [[ $skip == true ]] && continue # Skip if this module's kernel is installed.
  542.  
  543.             # If no installed package owns this module directory.
  544.             if ! pacman -Qo "/usr/lib/modules/${module_dir}" &>/dev/null; then
  545.                 orphans+=("${module_dir}") # Mark this module directory as orphaned.
  546.             fi
  547.         done
  548.  
  549.         # If no orphaned kernel modules found.
  550.         if [[ ${#orphans[@]} -eq 0 ]]; then
  551.             echo
  552.             echo -e "${BRIGHT_BLUE}::${RESET} No orphaned kernel modules found . . ."
  553.             return
  554.         fi
  555.  
  556.         echo
  557.         echo -e "${BRIGHT_BLUE}::${RESET} Remove orphaned kernel modules . . ."
  558.  
  559.         # Print installed kernels.
  560.         echo
  561.         echo -e "   ${BRIGHT_CYAN}Installed kernels:${RESET}"
  562.         for pkgname in "${!kernel_versions[@]}"; do
  563.             echo -e "       ${BRIGHT_CYAN}${pkgname}: ${kernel_versions[$pkgname]}${RESET}"
  564.         done
  565.  
  566.         # Print orphaned modules.
  567.         echo
  568.         echo -e "   ${BRIGHT_RED}Orphaned kernel modules:${RESET}"
  569.         for orphan in "${orphans[@]}"; do
  570.             echo -e "       ${BRIGHT_RED}\"/usr/lib/modules/${orphan}\"${RESET}"
  571.         done
  572.  
  573.         # Read DKMS status once and collect a list of unique module names.
  574.         local dkms_lines=()
  575.         local modules=()
  576.         if command -v dkms &>/dev/null; then
  577.             mapfile -t dkms_lines < <(dkms status)
  578.             mapfile -t modules < <(printf '%s\n' "${dkms_lines[@]}" | awk -F',' '{print $1}' | sort -u)
  579.         fi
  580.  
  581.         # Remove orphaned kernel modules.
  582.         local remove_all=false
  583.         for orphan in "${orphans[@]}"; do
  584.             echo
  585.             echo -e "   ${BRIGHT_YELLOW}Remove \"/usr/lib/modules/${orphan}\"${RESET}"
  586.  
  587.             # Show DKMS modules associated with this orphan.
  588.             matched_modules=()
  589.             if [[ ${#modules[@]} -gt 0 ]]; then
  590.                 # Loop through each unique module.
  591.                 for mod in "${modules[@]}"; do
  592.                     for line in "${dkms_lines[@]}"; do
  593.                         # If module is built for the orphaned kernel.
  594.                         if [[ "$line" == "${mod}, ${orphan},"* ]]; then
  595.                             matched_modules+=("$mod")
  596.                             break
  597.                         fi
  598.                     done
  599.                 done
  600.  
  601.                 # If DKMS modules exist which are associated with the orphaned kernel.
  602.                 if [[ ${#matched_modules[@]} -gt 0 ]]; then
  603.                     echo
  604.                     echo -e "   ${BRIGHT_YELLOW}Remove DKMS modules associated with '${orphan}':${RESET}"
  605.                     for mod in "${matched_modules[@]}"; do
  606.                         echo -e "       ${BRIGHT_YELLOW}${mod}${RESET}"
  607.                     done
  608.                 fi
  609.             fi
  610.  
  611.             # Ask the user if they want to delete or skip.
  612.             if [[ $remove_all == false ]]; then
  613.                 while true; do
  614.                     echo
  615.                     read -p "   Do you want to continue? [Y/N/A]: " response
  616.                     case "$response" in
  617.                         y|Y) break ;; # Remove this orphan.
  618.                         n|N) continue 2 ;; # Skip to next orphan.
  619.                         a|A) remove_all=true; break ;; # Remove all orphans.
  620.                         *) echo; echo "   Please answer Y, N, or A." ;;
  621.                     esac
  622.                 done
  623.             fi
  624.  
  625.             sudo rm -rf "/usr/lib/modules/${orphan}"
  626.  
  627.             # Remove DKMS records.
  628.             if [[ ${#matched_modules[@]} -gt 0 ]]; then
  629.                 for mod in "${matched_modules[@]}"; do
  630.                     echo -e "   ${BRIGHT_CYAN}Removing DKMS record for '${mod}' on '${orphan}'${RESET}"
  631.                     sudo dkms remove "${mod}" -k "${orphan}" --all
  632.                 done
  633.             fi
  634.         done
  635.  
  636.         echo
  637.         echo -e "   ${BRIGHT_GREEN}Orphaned kernel module cleanup complete . . .${RESET}"
  638.     }
  639.     remove_orphaned_kernel_modules
  640.  
  641.     is_system_bootable() {
  642.         # Find kernel images.
  643.         # local kernel_images=($(ls /boot/vmlinuz-* 2>/dev/null))
  644.         local kernel_images=($(shopt -s nullglob; echo /boot/vmlinuz-*))
  645.         if [[ ${#kernel_images[@]} -eq 0 ]]; then
  646.             echo
  647.             echo -e "${MAGENTA}:: WARNING: No kernel images found in /boot${RESET}"
  648.             exit 1
  649.         fi
  650.  
  651.         # Create a list of expected initramfs files based on the list of detected kernels.
  652.         local initramfs_images=()
  653.         for kernel in "${kernel_images[@]}"; do
  654.             local base_name="${kernel#/boot/vmlinuz-}"
  655.             initramfs_images+=("/boot/initramfs-${base_name}.img")
  656.             initramfs_images+=("/boot/initramfs-${base_name}-fallback.img")
  657.         done
  658.  
  659.         # Check for missing kernel and initramfs files.
  660.         local missing_files=()
  661.         for file in "${kernel_images[@]}" "${initramfs_images[@]}"; do
  662.             [[ ! -f "$file" ]] && missing_files+=("$file")
  663.         done
  664.  
  665.         # Check for missing bootloader files.
  666.         if is_grub_installed; then
  667.             local grub_cfg
  668.             grub_cfg=$(find /boot /efi /EFI -type f -path "*/grub/grub.cfg" -print -quit 2>/dev/null)
  669.             [[ -z "$grub_cfg" ]] && missing_files+=("esp/grub.cfg")
  670.         elif is_refind_installed; then
  671.             local refind_conf
  672.             refind_conf=$(find /boot /efi /EFI -type f -path "*/refind/refind.conf" -print -quit 2>/dev/null)
  673.             [[ -z "$refind_conf" ]] && missing_files+=("esp/refind/refind.conf")
  674.         elif is_systemd_boot_installed; then
  675.             local loader_conf
  676.             loader_conf=$(find /boot /efi /EFI -type f -path "*/loader/loader.conf" -print -quit 2>/dev/null)
  677.             [[ -z "$loader_conf" ]] && missing_files+=("esp/loader/loader.conf")
  678.             if ! find /boot /efi /EFI -type f -path "*/loader/entries/*.conf" -quit &>/dev/null; then
  679.                 missing_files+=("esp/loader/entries/*.conf")
  680.             fi
  681.         fi
  682.  
  683.         if [[ ${#missing_files[@]} -ne 0 ]]; then
  684.             echo
  685.             echo -e "${MAGENTA}:: WARNING: System boot files are missing:${RESET}"
  686.             for file in "${missing_files[@]}"; do
  687.                 echo -e "${MAGENTA}   $file${RESET}"
  688.             done
  689.             echo
  690.             echo -e "${BOLD}${GREEN}   Suggested repairs:${RESET}"
  691.             echo
  692.             echo -e "${BOLD}${GREEN}   $ sudo mkinitcpio -P${RESET}"
  693.             echo
  694.             if is_grub_installed; then
  695.                 [[ -z "$grub_cfg" ]] && echo -e "${BOLD}${GREEN}   $ sudo grub-mkconfig -o /boot/grub/grub.cfg${RESET}" && echo
  696.             elif is_refind_installed; then
  697.                 [[ -z "$refind_conf" ]] && echo -e "${BOLD}${GREEN}   $ sudo refind-install${RESET}" && echo
  698.             elif is_systemd_boot_installed; then
  699.                 [[ -z "$loader_conf" ]] && echo -e "${BOLD}${GREEN}   $ sudo bootctl update${RESET}" && echo
  700.             else
  701.                 echo -e "${BOLD}${GREEN}   Reinstall your boot loader.${RESET}" && echo
  702.             fi
  703.             exit 1
  704.         fi
  705.  
  706.         echo
  707.         echo -e "${BRIGHT_BLUE}::${RESET} No boot issues detected . . ."
  708.     }
  709.     is_system_bootable
  710.  
  711. File: /usr/local/bin/yap
  712. USAGE: $ yap    <-- Show recent Arch Linux news events, highlighting events since the last system upgrade.
  713.  
  714.     #!/usr/bin/env python3
  715.  
  716.     # Installation:
  717.     #     $ sudo cp ./yap /usr/local/bin/
  718.     #     $ sudo chmod +x /usr/local/bin/yap
  719.  
  720.     # ----- Colors -----
  721.     BLACK = '\033[30m'
  722.     RED = '\033[31m'
  723.     GREEN = '\033[32m'
  724.     YELLOW = '\033[33m'
  725.     BLUE = '\033[34m'
  726.     MAGENTA = '\033[35m'
  727.     CYAN = '\033[36m'
  728.     WHITE = '\033[37m'
  729.     BRIGHT_BLACK = '\033[90m'
  730.     BRIGHT_RED = '\033[91m'
  731.     BRIGHT_GREEN = '\033[92m'
  732.     BRIGHT_YELLOW = '\033[93m'
  733.     BRIGHT_BLUE = '\033[94m'
  734.     BRIGHT_MAGENTA = '\033[95m'
  735.     BRIGHT_CYAN = '\033[96m'
  736.     BRIGHT_WHITE = '\033[97m'
  737.  
  738.     # ----- Styles -----
  739.     RESET = '\033[0m' # Reset all styles.
  740.     BOLD = '\033[1m'
  741.     HALF_BRIGHT = '\033[2m'
  742.     ITALIC = '\033[3m'
  743.     UNDERLINE = '\033[4m'
  744.     REVERSED = '\033[7m' # Reverse foreground and background colors.
  745.  
  746.     import html
  747.     import re
  748.     import shutil
  749.     import subprocess
  750.     import sys
  751.     import time
  752.     import xml.etree.ElementTree as ET
  753.     from datetime import datetime
  754.     from pathlib import Path
  755.     from urllib.error import URLError, HTTPError
  756.     from urllib.request import urlopen, Request
  757.  
  758.     NEWS_FEED_URL = "https://archlinux.org/feeds/news/"
  759.     PACMAN_LOG = "/var/log/pacman.log"
  760.  
  761.     import argparse
  762.     def usage():
  763.         parser = argparse.ArgumentParser(
  764.             description="Show recent Arch Linux news events, highlighting ones since your last system upgrade.",
  765.             add_help=False
  766.         )
  767.         parser.add_argument("--brief", action="store_true", help="Show titles and URLs only (omit descriptions).")
  768.         parser.add_argument("--current", action="store_true", help="Only show news since your last full upgrade.")
  769.         parser.add_argument("--max", type=int, default=0, help="Limit number of news items shown.")
  770.         parser.add_argument("--debug", action="store_true", help="Force all news to appear new (for testing).")
  771.         parser.add_argument("--help", "-h", action="help", help="Show this help message and exit.")
  772.  
  773.         try:
  774.             global args
  775.             args = parser.parse_args()
  776.         except SystemExit:
  777.             sys.exit(0) # No news.
  778.  
  779.     def get_last_upgrade_time(debug=False):
  780.         if debug:
  781.             return 0
  782.  
  783.         try:
  784.             with open(PACMAN_LOG, "r") as f:
  785.                 for line in reversed(f.readlines()):
  786.                     if "starting full system upgrade" in line:
  787.                         ts_match = re.search(r"\[(.*?)\]", line)
  788.                         if ts_match:
  789.                             ts_str = ts_match.group(1)
  790.                             return int(datetime.strptime(ts_str, "%Y-%m-%dT%H:%M:%S%z").timestamp())
  791.         except Exception:
  792.             pass
  793.  
  794.         return 0 # Fallback if no match.
  795.  
  796.     def fetch_rss_feed(url):
  797.         try:
  798.             req = Request(url, headers={"User-Agent": "Mozilla/5.0"})
  799.             with urlopen(req, timeout=5) as response:
  800.                 return response.read().decode("utf-8")
  801.         except HTTPError as e:
  802.             print(f"{BRIGHT_RED}HTTP Error {e.code}: {e.reason}{RESET}", file=sys.stderr)
  803.         except URLError as e:
  804.             print(f"{BRIGHT_RED}URL Error: {e.reason}{RESET}", file=sys.stderr)
  805.         return None
  806.  
  807.     def simplify_links(text):
  808.         #  # Convert <a href="url">text</a> to [text](url)
  809.         #  text = re.sub(r'<a[^>]+href=["\']([^"\']+)["\'][^>]*>(.*?)</a>', r'[\2](\1)', text)
  810.         #  # Simplify [text](url) to just text if text == url
  811.         #  text = re.sub(r'\[([^\]]+)\]\(([^)]+)\)', lambda m: m.group(1) if m.group(1) == m.group(2) else m.group(0), text)
  812.  
  813.         # Convert <a href="url">text</a> to [text](url), or just text if text == url.
  814.         text = re.sub(
  815.             r'<a[^>]+href=["\']([^"\']+)["\'][^>]*>(.*?)</a>',
  816.            lambda m: m.group(1) if m.group(1) == m.group(2) else f'[{m.group(2)}]({m.group(1)})',
  817.            text
  818.        )
  819.  
  820.        # Unescape HTML entities recursively.
  821.        prev = None
  822.        while prev != text:
  823.            prev = text
  824.            text = html.unescape(text)
  825.  
  826.        # Strip remaining HTML tags.
  827.        text = re.sub(r"<[^>]+>", "", text)
  828.  
  829.        return text.strip()
  830.  
  831.    def parse_feed(xml_data):
  832.        root = ET.fromstring(xml_data)
  833.        channel = root.find("channel")
  834.        return channel.findall("item") if channel is not None else []
  835.  
  836.    def display_news(items, last_upgrade, brief=False, current_only=False, max_events=0):
  837.        now = int(time.time())
  838.        count = 0
  839.  
  840.        for item in items:
  841.            title = simplify_links(item.findtext("title", default=""))
  842.            link = simplify_links(item.findtext("link", default=""))
  843.            desc = simplify_links(item.findtext("description", default=""))
  844.            pub_date = item.findtext("pubDate", default="")
  845.  
  846.            try:
  847.                pub_ts = int(datetime.strptime(pub_date, "%a, %d %b %Y %H:%M:%S %z").timestamp())
  848.            except Exception:
  849.                continue
  850.  
  851.            if current_only and pub_ts <= last_upgrade:
  852.                continue
  853.  
  854.            color = BRIGHT_RED if pub_ts > last_upgrade else BRIGHT_GREEN
  855.            status = "[NEW] " if pub_ts > last_upgrade else ""
  856.            days_ago = int((now - pub_ts) / (60 * 60 * 24) + 0.5)
  857.            iso_date = datetime.fromtimestamp(pub_ts).strftime("%Y-%m-%d")
  858.  
  859.            print(f"\n{color}{status}D+{days_ago:03}/{iso_date} {title}{RESET}")
  860.            print(f"{color}@ {RESET}{link}")
  861.            if not brief:
  862.                print(f"{color}>{RESET} {desc}")
  863.  
  864.            count += 1
  865.            if max_events > 0 and count >= max_events:
  866.                break
  867.  
  868.        return count > 0
  869.  
  870.    def wait_for_any_keypress():
  871.        if sys.platform == 'win32':
  872.            import os
  873.            os.system('pause')
  874.        elif sys.platform.startswith('linux') or sys.platform == 'darwin':
  875.            print('Press any key to continue . . .')
  876.            import termios
  877.            import tty
  878.            stdin_file_desc = sys.stdin.fileno()
  879.            old_stdin_tty_attr = termios.tcgetattr(stdin_file_desc)
  880.            try:
  881.                tty.setraw(stdin_file_desc)
  882.                sys.stdin.read(1)
  883.            finally:
  884.                termios.tcsetattr(stdin_file_desc, termios.TCSADRAIN, old_stdin_tty_attr)
  885.  
  886.    def main():
  887.        usage()
  888.  
  889.        # Paging support.
  890.        if sys.stdout.isatty() and "YAP_IN_LESS" not in os.environ and len(sys.argv) == 1:
  891.            os.environ["YAP_IN_LESS"] = "1"
  892.            if shutil.which("less"):
  893.                pager = ["less", "-R"]
  894.                shell = False
  895.            else:
  896.                pager = None
  897.                shell = False
  898.  
  899.            if pager:
  900.                proc = subprocess.Popen([sys.executable] + sys.argv, stdout=subprocess.PIPE)
  901.                try:
  902.                    subprocess.run(pager, stdin=proc.stdout, shell=shell)
  903.                except KeyboardInterrupt:
  904.                    pass
  905.  
  906.                if sys.platform != 'win32':
  907.                    proc.wait()
  908.  
  909.                    code = proc.returncode
  910.                    if code in (0, 2): # Preserve 0 (no news) / 1 (error) / 2 (news) codes.
  911.                        sys.exit(code)
  912.                    else: # Convert all other return codes e.g. if the child process was killed by a signal, proc.returncode will be negative, to 1 (error).
  913.                        sys.exit(1) # Error.
  914.                else:
  915.                    sys.exit(0) # No news.
  916.  
  917.        # Fetch feed and display.
  918.        feed = fetch_rss_feed(NEWS_FEED_URL)
  919.        if feed is None:
  920.            sys.exit(1) # Error.
  921.  
  922.        last_upgrade = 0 if args.debug else get_last_upgrade_time()
  923.  
  924.        from datetime import datetime
  925.        from time import time
  926.        upgrade_iso = datetime.fromtimestamp(last_upgrade).isoformat()
  927.        days_ago = int((time() - last_upgrade) / (60 * 60 * 24) + 0.5)
  928.        print(f"{BRIGHT_GREEN}Last full system upgrade:{RESET} D+{days_ago:03}/{upgrade_iso}")
  929.  
  930.        items = parse_feed(feed)
  931.        try:
  932.            success = display_news(items, last_upgrade, brief=args.brief, current_only=args.current, max_events=args.max)
  933.        except BrokenPipeError:
  934.            try:
  935.                sys.stdout.close()
  936.            except Exception:
  937.                pass
  938.            finally:
  939.                sys.exit(0) # No news. The user quit by pressing Q.
  940.  
  941.        if success:
  942.            sys.exit(2) # News.
  943.        else:
  944.            if args.current:
  945.                print("No news detected since last upgrade")
  946.            else:
  947.                print("No news items found in the feed")
  948.            sys.exit(0) # No news.
  949.  
  950.    if __name__ == "__main__":
  951.        import os
  952.        main()
  953.  
  954. File: /usr/local/bin/yow
  955. USAGE: $ yow    <-- Show a colorized version of /var/log/pacman.log.
  956.  
  957.    #!/bin/bash
  958.  
  959.    # Installation:
  960.    #     $ sudo cp ./yow /usr/local/bin/
  961.    #     $ sudo chmod +x /usr/local/bin/yow
  962.  
  963.    # ----- Colors -----
  964.    BLACK="\033[30m"
  965.    RED="\033[31m"
  966.    GREEN="\033[32m"
  967.    YELLOW="\033[33m"
  968.    BLUE="\033[34m"
  969.    MAGENTA="\033[35m"
  970.    CYAN="\033[36m"
  971.    WHITE="\033[37m"
  972.    BRIGHT_BLACK="\033[90m"
  973.    BRIGHT_RED="\033[91m"
  974.    BRIGHT_GREEN="\033[92m"
  975.    BRIGHT_YELLOW="\033[93m"
  976.    BRIGHT_BLUE="\033[94m"
  977.    BRIGHT_MAGENTA="\033[95m"
  978.    BRIGHT_CYAN="\033[96m"
  979.    BRIGHT_WHITE="\033[97m"
  980.  
  981.    # ----- Styles -----
  982.    RESET="\033[0m" # Reset all styles.
  983.    BOLD="\033[1m"
  984.    HALF_BRIGHT="\033[2m"
  985.    ITALIC="\033[3m"
  986.    UNDERLINE="\033[4m"
  987.    REVERSED="\033[7m" # Reverse foreground/background.
  988.  
  989.    PACMAN_LOG='/var/log/pacman.log'
  990.  
  991.    # ----- Members -----
  992.    display_help() {
  993.        cat <<EOF
  994.    Display and colorize $PACMAN_LOG entries.
  995.  
  996.    Usage: $ yow [OPTION]
  997.  
  998.    NOTE: If no option is provided then today's $(date +%Y-%m-%d) pacman.log entries will be displayed.
  999.  
  1000.     Options:
  1001.         --all                    Show the entire pacman log colorized.
  1002.  
  1003.         --date=YYYY-MM-DD        Show entries for the given date.
  1004.         --day=N                  Show entries for N days ago.
  1005.         --day=today|yesterday    Show entries for the named day.
  1006.  
  1007.         --since=YYYY-MM-DD       Show entries starting from the given date.
  1008.         --since=N                Show entries starting from N days ago.
  1009.         --since=today|yesterday  Show entries starting from the named day.
  1010.  
  1011.         --today                  Alias for --day=today.
  1012.         --yesterday              Alias for --day=yesterday.
  1013.  
  1014.         -h, --help               Show this help message and exit.
  1015.     EOF
  1016.     }
  1017.  
  1018.     colorize_line() {
  1019.         local line="$1"
  1020.         case "$line" in
  1021.             *"error:"*|*"warning:"*)
  1022.                 echo -e "${BRIGHT_YELLOW}${line}${RESET}" ;;
  1023.             *"[ALPM-SCRIPTLET]"*|*".pacnew"*|*".pacsave"*|*".pacorig"*)
  1024.                 echo -e "${BRIGHT_BLACK}${line}${RESET}" ;;
  1025.             *"[ALPM] installed "*|*"[ALPM] upgraded "*)
  1026.                 echo -e "${BRIGHT_CYAN}${line}${RESET}" ;;
  1027.             *"[ALPM] removed "*)
  1028.                 echo -e "${BRIGHT_RED}${line}${RESET}" ;;
  1029.             *"[ALPM] downgraded "*)
  1030.                 echo -e "${BRIGHT_MAGENTA}${line}${RESET}" ;;
  1031.             *)
  1032.                 echo -e "$line" ;;
  1033.         esac
  1034.     }
  1035.  
  1036.     normalize_date() {
  1037.         local input="$1"
  1038.         date -d "$input" +%Y-%m-%d 2>/dev/null || return 1
  1039.     }
  1040.  
  1041.     resolve_relative_date() {
  1042.         local value="$1"
  1043.         case "$value" in
  1044.             today) date +%Y-%m-%d ;;
  1045.             yesterday) date -d "yesterday" +%Y-%m-%d ;;
  1046.             ''|*[!0-9]*) echo "$value" ;; # Already a date string like "2025-08-01".
  1047.             *) date -d "$value days ago" +%Y-%m-%d ;;
  1048.         esac
  1049.     }
  1050.  
  1051.     create_date_label() {
  1052.         local log_date="$1"
  1053.         local now_ts log_ts days_diff
  1054.  
  1055.         now_ts=$(date +%s)
  1056.         log_ts=$(date -d "$log_date" +%s 2>/dev/null) || return
  1057.  
  1058.         days_diff=$(( (log_ts - now_ts) / 86400 ))
  1059.  
  1060.         case $days_diff in
  1061.             -1) echo "YESTERDAY/" ;;
  1062.             0)  echo "TODAY/" ;;
  1063.             1)  echo "TOMORROW/" ;;
  1064.             *)
  1065.                 if (( days_diff < 0 )); then
  1066.                     echo "D-${days_diff#-}/"
  1067.                 else
  1068.                     echo "D+${days_diff}/"
  1069.                 fi ;;
  1070.         esac
  1071.     }
  1072.  
  1073.     display_pacman_log() {
  1074.         local log_date="$1"
  1075.         local mode="${2:-exact}"
  1076.  
  1077.         if [[ ! -s "$PACMAN_LOG" ]]; then
  1078.             echo "ERROR: Cannot read or empty log: $PACMAN_LOG" >&2
  1079.             exit 1
  1080.         fi
  1081.  
  1082.         if [[ "$mode" == "since" ]]; then
  1083.             local current_date="$log_date"
  1084.             local today=$(date +%Y-%m-%d)
  1085.             local end_date=$(date -d "$today +1 day" +%Y-%m-%d)
  1086.  
  1087.             {
  1088.                 while [[ "$current_date" < "$end_date" ]]; do
  1089.                     local label=$(create_date_label "$current_date")
  1090.                     local header="${BRIGHT_BLUE}::${RESET} Viewing $PACMAN_LOG entries for ${label}${current_date}"
  1091.                     local matches
  1092.                     matches=$(grep -F -- "$current_date" "$PACMAN_LOG" 2>/dev/null || true)
  1093.  
  1094.                     if [[ -n "$matches" ]]; then
  1095.                         echo -e "${header}\n"
  1096.                         echo "$matches"
  1097.                         echo
  1098.                     fi
  1099.  
  1100.                     current_date=$(date -d "$current_date +1 day" +%Y-%m-%d)
  1101.                 done
  1102.             } | while IFS= read -r line; do
  1103.                 colorize_line "$line"
  1104.             done | less -R
  1105.  
  1106.         else
  1107.             local label=$(create_date_label "$log_date")
  1108.             local header="${BRIGHT_BLUE}::${RESET} Viewing $PACMAN_LOG entries for ${label}${log_date}"
  1109.             local matches=$(grep -F -- "$log_date" "$PACMAN_LOG")
  1110.  
  1111.             if [[ -z "$matches" ]]; then
  1112.                 echo -e "${BRIGHT_BLUE}::${RESET} No entries found for ${label}${log_date} in $PACMAN_LOG"
  1113.                 exit 0
  1114.             fi
  1115.  
  1116.             {
  1117.                 echo -e "${header}\n"
  1118.                 echo "$matches"
  1119.             } | while IFS= read -r line; do
  1120.                 colorize_line "$line"
  1121.             done | less -R
  1122.         fi
  1123.     }
  1124.  
  1125.     # ----- Parse arguments -----
  1126.     parse_date_arg() {
  1127.         local key="$1"
  1128.         local value="$2"
  1129.  
  1130.         if [[ -z "$value" ]]; then
  1131.             echo "ERROR: Missing value for --$key" >&2
  1132.             exit 1
  1133.         fi
  1134.  
  1135.         value=$(resolve_relative_date "$value")
  1136.         if ! norm=$(normalize_date "$value"); then
  1137.             echo "ERROR: Invalid date format: '$value'" >&2
  1138.             exit 1
  1139.         fi
  1140.  
  1141.         [[ "$key" == "since" ]] && mode="since" || mode="exact"
  1142.         display_pacman_log "$norm" "$mode"
  1143.         exit 0
  1144.     }
  1145.  
  1146.     # --key=value handling.
  1147.     if [[ "${1:-}" =~ ^--(date|day|since)=(.*)$ ]]; then
  1148.         parse_date_arg "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}"
  1149.     fi
  1150.  
  1151.     # --key value handling.
  1152.     case "${1:-}" in
  1153.         -h|--help)
  1154.             display_help ;;
  1155.         --all)
  1156.             first_date=$(grep -o '^\[[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}' "$PACMAN_LOG" | head -n1 | tr -d '[')
  1157.             if ! norm=$(normalize_date "$first_date"); then
  1158.                 echo "ERROR: Could not determine start date of $PACMAN_LOG" >&2
  1159.                 exit 1
  1160.             fi
  1161.             display_pacman_log "$norm" since ;;
  1162.         --today|"")
  1163.             display_pacman_log "$(date +%Y-%m-%d)" exact ;;
  1164.         --yesterday)
  1165.             display_pacman_log "$(date -d yesterday +%Y-%m-%d)" exact ;;
  1166.         --date|--since|--day)
  1167.             key="${1#--}"
  1168.             shift
  1169.             if [[ -z "${1:-}" ]]; then
  1170.                 echo "ERROR: Missing value for --$key" >&2
  1171.                 exit 1
  1172.             fi
  1173.             value="$1"
  1174.             parse_date_arg "$key" "$value" ;;
  1175.         *)
  1176.             echo "ERROR: Unknown option '${1:-}'" >&2
  1177.             echo "Try '$ yow --help' for usage." >&2
  1178.             exit 1 ;;
  1179.     esac
  1180.  
  1181. File: /usr/local/bin/tug
  1182. USAGE: $ tug    <-- Updates my vim/nvim plugins.
  1183.  
  1184.     #!/bin/bash
  1185.  
  1186.     # Installation:
  1187.     #     $ sudo cp ./tug /usr/local/bin/
  1188.     #     $ sudo chmod +x /usr/local/bin/tug
  1189.  
  1190.     # ----- Colors -----
  1191.     BLACK="\033[30m"
  1192.     RED="\033[31m"
  1193.     GREEN="\033[32m"
  1194.     YELLOW="\033[33m"
  1195.     BLUE="\033[34m"
  1196.     MAGENTA="\033[35m"
  1197.     CYAN="\033[36m"
  1198.     WHITE="\033[37m"
  1199.     BRIGHT_BLACK="\033[90m"
  1200.     BRIGHT_RED="\033[91m"
  1201.     BRIGHT_GREEN="\033[92m"
  1202.     BRIGHT_YELLOW="\033[93m"
  1203.     BRIGHT_BLUE="\033[94m"
  1204.     BRIGHT_MAGENTA="\033[95m"
  1205.     BRIGHT_CYAN="\033[96m"
  1206.     BRIGHT_WHITE="\033[97m"
  1207.  
  1208.     # ----- Styles -----
  1209.     RESET="\033[0m" # Reset all styles.
  1210.     BOLD="\033[1m"
  1211.     HALF_BRIGHT="\033[2m"
  1212.     ITALIC="\033[3m"
  1213.     UNDERLINE="\033[4m"
  1214.     REVERSED="\033[7m" # Reverse foreground/background.
  1215.  
  1216.     # Prompt for sudo at the start.
  1217.     if ! sudo -v; then
  1218.         echo -e "${BRIGHT_BLUE}::${RESET} This script requires sudo privileges. Exiting."
  1219.         exit 1
  1220.     fi
  1221.  
  1222.     update_vim_plugins() {
  1223.         # ----- Vim -----
  1224.         if command -v vim &>/dev/null && [ -f ~/.vim/autoload/plug.vim ]; then
  1225.             echo -e "${BRIGHT_BLUE}::${RESET} Updating user vim plugins . . ."
  1226.             # Update ~/.vim/ plugins.
  1227.             vim -es +'silent! PlugUpgrade' +'silent! PlugUpdate' +qall
  1228.             echo
  1229.  
  1230.             if sudo -n true 2>/dev/null; then
  1231.                 echo -e "${BRIGHT_BLUE}::${RESET} Updating root vim plugins . . ."
  1232.                 # Update /root/.vim/ plugins.
  1233.                 # -E is needed so the environment variables are available to the sudo instance of vim.
  1234.                 # -H is needed so $HOME=/root instead of ~/home/wolf (a side effect of using the -E option).
  1235.                 sudo -EH vim -es +'silent! PlugUpgrade' +'silent! PlugUpdate' +qall
  1236.             else
  1237.                 echo -e "${BRIGHT_BLUE}::${RESET} Skipping root vim plugins (no sudo)."
  1238.             fi
  1239.         fi
  1240.  
  1241.         # ----- Neovim -----
  1242.         if command -v nvim &>/dev/null && [ -f ~/.local/share/nvim/site/autoload/plug.vim ]; then
  1243.             echo
  1244.             echo -e "${BRIGHT_BLUE}::${RESET} Updating user nvim plugins . . ."
  1245.             nvim --headless +'silent! PlugUpgrade' +'silent! PlugUpdate' +qall
  1246.             echo
  1247.  
  1248.             if sudo -n true 2>/dev/null; then
  1249.                 echo -e "${BRIGHT_BLUE}::${RESET} Updating root nvim plugins . . ."
  1250.                 sudo -EH nvim --headless +'silent! PlugUpgrade' +'silent! PlugUpdate' +qall
  1251.             else
  1252.                 echo -e "${BRIGHT_BLUE}::${RESET} Skipping root nvim plugins (no sudo)."
  1253.             fi
  1254.         fi
  1255.     }
  1256.     update_vim_plugins
  1257.  
Advertisement
Add Comment
Please, Sign In to add comment