FocusedWolf

Arch: pacman+yay scripts to update like a boss

Feb 26th, 2024 (edited)
2,228
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Bash 61.60 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 json
  37.     import math
  38.     import os
  39.     import shutil
  40.     import subprocess
  41.     import sys
  42.     import threading
  43.     import time
  44.     import urllib.request
  45.  
  46.     EARTH_RADIUS_KM = 6371.0
  47.     MIN_DESIRED_MIRRORS = 20
  48.  
  49.     CURRENT_MIRRORLIST_PATH = '/etc/pacman.d/mirrorlist'
  50.     BACKUP_MIRRORLIST_PATH = f'{CURRENT_MIRRORLIST_PATH}.backup'
  51.     TEMP_MIRRORLIST_PATH = f'{CURRENT_MIRRORLIST_PATH}.new'
  52.  
  53.     def sudo_keep_alive():
  54.         """Silently updates the sudo timestamp in the background."""
  55.         while True:
  56.             subprocess.run(['sudo', '-n', 'true'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
  57.             time.sleep(60)
  58.  
  59.     def authenticate_sudo():
  60.         """Prompts for sudo upfront and starts the background keep-alive thread."""
  61.         if subprocess.run(['sudo', '-v']).returncode != 0:
  62.             print(f"{RED}::{RESET} This script requires sudo privileges. Exiting.")
  63.             sys.exit(1)
  64.  
  65.         # daemon=True ensures this thread dies instantly when the script exits
  66.         keep_alive = threading.Thread(target=sudo_keep_alive, daemon=True)
  67.         keep_alive.start()
  68.  
  69.     # Generated country coordinates (lat, lon).
  70.     COUNTRY_COORDS = {
  71.         'Argentina': (-34, -64),
  72.         'Armenia': (40, 45),
  73.         'Australia': (-27, 133),
  74.         'Austria': (47.33333333, 13.33333333),
  75.         'Azerbaijan': (40.5, 47.5),
  76.         'Bangladesh': (24, 90),
  77.         'Belarus': (53, 28),
  78.         'Belgium': (50.83333333, 4),
  79.         'Brazil': (-10, -55),
  80.         'Bulgaria': (43, 25),
  81.         'Cambodia': (13, 105),
  82.         'Canada': (60, -95),
  83.         'Chile': (-30, -71),
  84.         'China': (35, 105),
  85.         'Colombia': (4, -72),
  86.         'Croatia': (45.16666666, 15.5),
  87.         'Czechia': (49.75, 15.5),
  88.         'Denmark': (56, 10),
  89.         'Ecuador': (-2, -77.5),
  90.         'Estonia': (59, 26),
  91.         'Finland': (64, 26),
  92.         'France': (46, 2),
  93.         'Georgia': (42, 43.5),
  94.         'Germany': (51, 9),
  95.         'Greece': (39, 22),
  96.         'Hong Kong': (22.267, 114.188),
  97.         'Hungary': (47, 20),
  98.         'Iceland': (65, -18),
  99.         'India': (20, 77),
  100.         'Indonesia': (-5, 120),
  101.         'Iran': (32, 53),
  102.         'Israel': (31.47, 35.13),
  103.         'Italy': (42.83333333, 12.83333333),
  104.         'Japan': (36, 138),
  105.         'Kazakhstan': (48, 68),
  106.         'Kenya': (1, 38),
  107.         'Latvia': (57, 25),
  108.         'Lithuania': (56, 24),
  109.         'Luxembourg': (49.75, 6.16666666),
  110.         'Malaysia': (2.5, 112.5),
  111.         'Mauritius': (-20.28333333, 57.55),
  112.         'Mexico': (23, -102),
  113.         'Moldova': (47, 29),
  114.         'Morocco': (32, -5),
  115.         'Nepal': (28, 84),
  116.         'Netherlands': (52.5, 5.75),
  117.         'New Caledonia': (-21.5, 165.5),
  118.         'New Zealand': (-41, 174),
  119.         'North Macedonia': (41.83333333, 22),
  120.         'Norway': (62, 10),
  121.         'Paraguay': (-23, -58),
  122.         'Philippines': (13, 122),
  123.         'Poland': (52, 20),
  124.         'Portugal': (39.5, -8),
  125.         'Romania': (46, 25),
  126.         'Russia': (60, 100),
  127.         'Réunion': (-21.15, 55.5),
  128.         'Saudi Arabia': (25, 45),
  129.         'Serbia': (44, 21),
  130.         'Singapore': (1.36666666, 103.8),
  131.         'Slovakia': (48.66666666, 19.5),
  132.         'Slovenia': (46.11666666, 14.81666666),
  133.         'South Africa': (-29, 24),
  134.         'South Korea': (37, 127.5),
  135.         'Spain': (40, -4),
  136.         'Sweden': (62, 15),
  137.         'Switzerland': (47, 8),
  138.         'Taiwan': (23.5, 121),
  139.         'Thailand': (15, 100),
  140.         'Tunisia': (34, 9),
  141.         'Türkiye': (39, 35),
  142.         'Ukraine': (49, 32),
  143.         'United Arab Emirates': (24, 54),
  144.         'United Kingdom': (54, -2),
  145.         'United States': (38, -97),
  146.         'Uzbekistan': (41, 64),
  147.         'Vietnam': (16.16666666, 107.83333333),
  148.     }
  149.  
  150.     # Calculate the great-circle distance between two points on a sphere using the Haversine formula.
  151.     def haversine_distance(lat1, lon1, lat2, lon2, radius):
  152.         # Convert latitude and longitude from degrees to radians.
  153.         lat1 = math.radians(lat1)
  154.         lon1 = math.radians(lon1)
  155.         lat2 = math.radians(lat2)
  156.         lon2 = math.radians(lon2)
  157.  
  158.         # Calculate the differences.
  159.         dlat = lat2 - lat1
  160.         dlon = lon2 - lon1
  161.  
  162.         # Haversine formula.
  163.         # SOURCE: https://www.movable-type.co.uk/scripts/latlong.html
  164.         # The square of half the chord length between the points.
  165.         a = math.sin(dlat / 2)**2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon / 2)**2
  166.         # The angular distance in radians.
  167.         c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
  168.         d = radius * c
  169.         return d
  170.  
  171.     # Generate a list of nearest countries.
  172.     def generate_sorted_countries_list(lat, lon, max_countries=5):
  173.         distances = []
  174.         for country, (country_lat, country_lon) in COUNTRY_COORDS.items():
  175.             dist = haversine_distance(lat, lon, country_lat, country_lon, EARTH_RADIUS_KM)
  176.             distances.append((dist, country))
  177.  
  178.         distances.sort(key=lambda x: x[0])
  179.         nearest = [country for _, country in distances[:max_countries]]
  180.         return ','.join(nearest)
  181.  
  182.     # Count mirrors in a file.
  183.     def count_mirrors(path):
  184.         try:
  185.             with open(path, 'r') as f:
  186.                 return sum(1 for line in f if line.strip().startswith('Server ='))
  187.         except FileNotFoundError:
  188.             return 0
  189.  
  190.     # Backup the current mirror list and update to a new one.
  191.     def update_mirrorlist(source_path):
  192.         if not os.path.exists(source_path):
  193.             print(f'{RED}::{RESET} ERROR: Temp mirror list not found: {source_path}')
  194.             print(f'{RED}   Keeping existing mirror list.{RESET}')
  195.             return False
  196.  
  197.         # Backup using sudo (using -p to preserve attributes of original fiile).
  198.         backup_cmd = ['sudo', 'cp', '-p', CURRENT_MIRRORLIST_PATH, BACKUP_MIRRORLIST_PATH]
  199.         if subprocess.call(backup_cmd) != 0:
  200.             print(f'{RED}::{RESET} ERROR: Backup failed. Aborting mirror list update.{RESET}')
  201.             return False
  202.  
  203.         print(f'{BRIGHT_BLUE}::{RESET} Backup created of the current mirror list: {BACKUP_MIRRORLIST_PATH}')
  204.  
  205.         # Replace using sudo (atomic mv).
  206.         replace_cmd = ['sudo', 'mv', source_path, CURRENT_MIRRORLIST_PATH]
  207.         if subprocess.call(replace_cmd) != 0:
  208.             print(f'{RED}::{RESET} ERROR: Failed to replace mirror list. Aborting.{RESET}')
  209.             print(f'{RED}   Your backup is still available at: {BACKUP_MIRRORLIST_PATH}{RESET}')
  210.             return False
  211.  
  212.         return True
  213.  
  214.     # Run reflector with closest countries.
  215.     def update_arch_mirrors(user_lat, user_lon):
  216.         if shutil.which('reflector') is None:
  217.             print(f'{RED}::{RESET} Reflector could not be found. Install with:')
  218.             print('   $ sudo pacman -S reflector')
  219.             sys.exit(1)
  220.  
  221.         # Remove temp file using sudo (root owns it)
  222.         subprocess.call(['sudo', 'rm', '-f', TEMP_MIRRORLIST_PATH])
  223.  
  224.         print(f'{BRIGHT_BLUE}::{RESET} Locating new pacman mirrors . . .')
  225.  
  226.         countries = generate_sorted_countries_list(user_lat, user_lon, 8)
  227.         print(f'{BRIGHT_CYAN}Nearby Countries:{RESET} {countries}')
  228.  
  229.         cmd = [
  230.             'sudo', 'reflector',
  231.             '--verbose',
  232.             '--country', countries,
  233.             '--protocol', 'https',
  234.             '--sort', 'rate',
  235.             '--age', '1',
  236.             '--save', TEMP_MIRRORLIST_PATH,
  237.         ]
  238.  
  239.         proc = subprocess.Popen(cmd)
  240.  
  241.         try:
  242.             proc.wait()
  243.         except KeyboardInterrupt:
  244.             # Gracefully stop reflector if the user hits Ctrl+C
  245.             proc.terminate()
  246.             proc.wait()
  247.             print(f'{RED}:: Mirror list update canceled by user . . .{RESET}')
  248.             sys.exit(1)
  249.  
  250.         if proc.returncode != 0:
  251.             print(f'{RED}:: Mirror list update failed (Reflector Return Code: {proc.returncode}). No changes made to mirror list.{RESET}')
  252.             sys.exit(1)
  253.  
  254.         mirror_count = count_mirrors(TEMP_MIRRORLIST_PATH)
  255.         print(f'{BRIGHT_BLUE}::{RESET} Reflector returned {mirror_count} mirrors.')
  256.  
  257.         if mirror_count == 0:
  258.             # Delete empty mirrorlist.
  259.             subprocess.call(['sudo', 'rm', '-f', TEMP_MIRRORLIST_PATH])
  260.             sys.exit(0)
  261.  
  262.         elif mirror_count < MIN_DESIRED_MIRRORS:
  263.             print(f'{RED}:: WARNING: Only {mirror_count} mirrors found but MIN_DESIRED_MIRRORS = {MIN_DESIRED_MIRRORS}.{RESET}')
  264.             choice = input(f'{BRIGHT_BLUE}::{RESET} Apply new mirror list located at "{TEMP_MIRRORLIST_PATH}" [y/N] ? ').strip().lower()
  265.             if choice != 'y':
  266.                 # TODO: Delete temp mirrorlist, or not so the user can look at it?
  267.                 #subprocess.call(['sudo', 'rm', '-f', TEMP_MIRRORLIST_PATH])
  268.                 sys.exit(0)
  269.  
  270.         if not update_mirrorlist(TEMP_MIRRORLIST_PATH):
  271.             sys.exit(1)
  272.  
  273.         print(f'{BRIGHT_GREEN}:: Mirror list updated successfully . . .{RESET}')
  274.  
  275.     def get_user_location():
  276.         # Fallback location: New York City
  277.         lat = 40.730610
  278.         lon = -73.935242
  279.  
  280.         try:
  281.             # 3-second timeout so the script doesn't hang if offline
  282.             with urllib.request.urlopen('http://ip-api.com/json/', timeout=3) as response:
  283.                 data = json.loads(response.read().decode())
  284.                 lat = data['lat']
  285.                 lon = data['lon']
  286.  
  287.                 # Safely extract the city and region for terminal output
  288.                 city = data.get('city', 'Unknown City')
  289.                 region = data.get('regionName', 'Unknown Region')
  290.                 print(f'{BRIGHT_BLUE}::{RESET} User location detected as: {city}, {region}')
  291.         except Exception:
  292.             print(f'{YELLOW}::{RESET} User location detection failed. Defaulting to New York City.')
  293.         return lat, lon
  294.  
  295.     def generate_country_coords():
  296.         # Ask reflector for supported countries.
  297.         result = subprocess.run(
  298.             ['reflector', '--list-countries'],
  299.             capture_output=True, text=True, check=True
  300.         )
  301.         reflector_lines = result.stdout.splitlines()[3:] # Skip headers.
  302.         valid_countries = {
  303.             ' '.join(line.split()[:-2]) for line in reflector_lines if line.strip()
  304.         }
  305.  
  306.         # Query the static, open-source country dataset directly from GitHub.
  307.         url = 'https://raw.githubusercontent.com/mledoze/countries/master/countries.json'
  308.         req = urllib.request.Request(
  309.             url,
  310.             headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'}
  311.         )
  312.  
  313.         try:
  314.             with urllib.request.urlopen(req) as response:
  315.                 data = json.loads(response.read().decode())
  316.         except urllib.error.URLError as e:
  317.             print(f"Error fetching country dataset: {e}")
  318.             return
  319.  
  320.         if not isinstance(data, list):
  321.             print(f"Dataset Error: Expected list, got {type(data)}")
  322.             return
  323.  
  324.         # Map country names to match reflector spelling.
  325.         NAME_MAP = {
  326.             # Not needed for Turkey anymore with this new source of country locations but
  327.             # still a good example for how to override if other countries become a problem.
  328.             # 'Turkey': 'Türkiye',
  329.         }
  330.  
  331.         # Print as Python dict.
  332.         print('COUNTRY_COORDS = {')
  333.         for country in sorted(data, key=lambda c: c.get('name', {}).get('common', '') if isinstance(c, dict) else ''):
  334.             if not isinstance(country, dict):
  335.                 continue
  336.             name = country.get('name', {}).get('common')
  337.             latlng = country.get('latlng')
  338.             if not (name and latlng) or len(latlng) < 2:
  339.                 continue
  340.             reflector_name = NAME_MAP.get(name, name)
  341.             if reflector_name in valid_countries:
  342.                 lat, lon = latlng[0], latlng[1]
  343.                 print(f"    '{reflector_name}': ({lat}, {lon}),")
  344.         print('}')
  345.  
  346.     if __name__ == '__main__':
  347.         # Uncomment to generate COUNTRY_COORDS dictionary.
  348.         # generate_country_coords()
  349.         # sys.exit(0)
  350.  
  351.         authenticate_sudo()
  352.         user_lat, user_lon = get_user_location()
  353.         update_arch_mirrors(user_lat, user_lon)
  354.  
  355. File: /usr/local/bin/yip
  356. USAGE: $ yip    <-- Update installed packages, delete downloaded packages, purge the orphans, and check if the system can boot, like a boss.
  357.  
  358.     #!/bin/bash
  359.  
  360.     # Installation:
  361.     #     $ sudo cp ./yip /usr/local/bin/
  362.     #     $ sudo chmod +x /usr/local/bin/yip
  363.  
  364.     # ----- Colors -----
  365.     BLACK="\033[30m"
  366.     RED="\033[31m"
  367.     GREEN="\033[32m"
  368.     YELLOW="\033[33m"
  369.     BLUE="\033[34m"
  370.     MAGENTA="\033[35m"
  371.     CYAN="\033[36m"
  372.     WHITE="\033[37m"
  373.     BRIGHT_BLACK="\033[90m"
  374.     BRIGHT_RED="\033[91m"
  375.     BRIGHT_GREEN="\033[92m"
  376.     BRIGHT_YELLOW="\033[93m"
  377.     BRIGHT_BLUE="\033[94m"
  378.     BRIGHT_MAGENTA="\033[95m"
  379.     BRIGHT_CYAN="\033[96m"
  380.     BRIGHT_WHITE="\033[97m"
  381.  
  382.     # ----- Styles -----
  383.     RESET="\033[0m" # Reset all styles.
  384.     BOLD="\033[1m"
  385.     HALF_BRIGHT="\033[2m"
  386.     ITALIC="\033[3m"
  387.     UNDERLINE="\033[4m"
  388.     REVERSED="\033[7m" # Reverse foreground/background.
  389.  
  390.     # ----- Configuration -----
  391.     PKG_MGR="sudo pacman"    # Repo packages only.
  392.     # PKG_MGR="yay"          # My favorite AUR helper.
  393.     # TODO: These might work. AI says they wrap pacman syntax close enough to work with this script but untested.
  394.     # PKG_MGR="paru"         # Rust-based AUR helper.
  395.     # PKG_MGR="pikaur"       # Python-based AUR helper.
  396.     # PKG_MGR="trizen"       # Lightweight Perl AUR helper.
  397.  
  398.     # Prompt for sudo at the start.
  399.     if ! sudo -v; then
  400.         echo -e "${BRIGHT_BLUE}::${RESET} This script requires sudo privileges. Exiting."
  401.         exit 1
  402.     fi
  403.  
  404.     # Keep-alive: update existing sudo time stamp in the background until the script has finished.
  405.     while true; do sudo -n true; sleep 60; kill -0 "$$" || exit; done 2>/dev/null &
  406.     REFRESH_PID=$!
  407.  
  408.     # Kill the background Keep-alive loop when this script exits.
  409.     # NOTE: If this fails the loop will kill itself after its sleep finishes.
  410.     trap 'kill $REFRESH_PID 2>/dev/null' EXIT
  411.  
  412.     # Get the directory containing this script.
  413.     SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  414.  
  415.    tty_check() {
  416.        # If running in a graphical session.
  417.        if [[ -n $DISPLAY ]]; then
  418.            while true; do
  419.                echo
  420.                echo -e "${CYAN}:: WARNING: Close ALL programs before continuing. Ideally you should boot to a TTY session before running this script.${RESET}"
  421.                echo -e "${CYAN}            Having programs running during an update can cause errors due to old libraries still being in use.${RESET}"
  422.                echo
  423.                read -p "Do you want to continue? [Y/n]: " response
  424.                # Set response to 'Y' if you didn't press Y|N so Enter allows you to continue like with Yay/Pacman.
  425.                response=${response:-Y}
  426.                case "$response" in
  427.                    y|Y) break ;;
  428.                    n|N) exit 1 ;; # Exit script with status 1 to indicate failure.
  429.                    *) echo; echo "Please answer Y or N." ;;
  430.                esac
  431.            done
  432.        fi
  433.    }
  434.    tty_check
  435.  
  436.    check_for_update_news() {
  437.        echo -e "${BRIGHT_BLUE}::${RESET} Checking Arch Linux RSS feed . . ."
  438.  
  439.        "$SCRIPT_DIR/yap" --current
  440.        status=$?
  441.  
  442.        case $status in
  443.            2)  # News detected.
  444.                echo
  445.                echo -e "${BRIGHT_CYAN}:: WARNING: News event detected.${RESET}"
  446.                ;;
  447.            1)  # Error detected.
  448.                # No need for a error message because the called script will display its own.
  449.                # echo -e "${BRIGHT_RED}:: ERROR: Could not fetch Arch Linux news!${RESET}
  450.                 ;;
  451.             0)  # No news or error detected.
  452.                 return
  453.                 ;;
  454.             *)  # Unexpected exit code.
  455.                 echo -e "${BRIGHT_RED}:: Unknown error occurred (exit code $status)!${RESET}"
  456.                 ;;
  457.         esac
  458.  
  459.         while true; do
  460.             echo
  461.             read -p "Do you want to continue? [Y/n]: " response
  462.             response=${response:-Y}
  463.             case "$response" in
  464.                 y|Y) break ;;
  465.                 n|N) exit 1 ;; # Exit script with status 1 to indicate failure.
  466.                 *) echo; echo "Please answer Y or N." ;;
  467.             esac
  468.         done
  469.     }
  470.     check_for_update_news
  471.  
  472.     free_space_check() {
  473.         local directory=$1
  474.         local low_space_mb=$2
  475.  
  476.         # If directory does not exist or is not a directory.
  477.         if [[ ! -d "$directory" ]]; then
  478.             echo
  479.             echo -e "${RED}::${RESET} ERROR: Directory '$directory' does not exist."
  480.             exit 1 # Exit script with status 1 to indicate failure.
  481.         fi
  482.  
  483.         # Get all space values in bytes.
  484.         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.
  485.         local free_space_bytes=$(df --block-size=1 --output=avail "$directory" 2>/dev/null | tail -n1)
  486.         local total_space_bytes=$(df --block-size=1 --output=size "$directory" 2>/dev/null | tail -n1)
  487.  
  488.         # If free space is less than low space.
  489.         if awk -v free="$free_space_bytes" -v low="$low_space_bytes" 'BEGIN { exit !(free < low) }'; then
  490.             local si_scalars=(1208925819614629174706176 1180591620717411303424 1152921504606846976 1125899906842624 1099511627776 1073741824 1048576 1024)
  491.             local si_prefixes=("YiB" "ZiB" "EiB" "PiB" "TiB" "GiB" "MiB" "KiB")
  492.  
  493.             # Find the appropriate SI prefix.
  494.             local scalar=1
  495.             local prefix="KiB"
  496.             for i in ${!si_scalars[@]}; do
  497.                 if awk -v space="$total_space_bytes" -v scalar="${si_scalars[$i]}" 'BEGIN { exit !(space >= scalar) }'; then
  498.                     scalar="${si_scalars[$i]}"
  499.                     prefix="${si_prefixes[$i]}"
  500.                     break
  501.                 fi
  502.             done
  503.  
  504.             # Calculate output space info.
  505.             local free_space_scaled=$(awk -v bytes="$free_space_bytes" -v scalar="$scalar" 'BEGIN { printf "%.1f", bytes / scalar }')
  506.             local total_space_scaled=$(awk -v bytes="$total_space_bytes" -v scalar="$scalar" 'BEGIN { printf "%.1f", bytes / scalar }')
  507.             local used_percent=$(awk -v free="$free_space_bytes" -v total="$total_space_bytes" 'BEGIN { printf "%.f", (1 - free / total) * 100 }')
  508.  
  509.             while true; do
  510.                 # Displays disk space info like Dolphin properties window.
  511.                 echo
  512.                 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}"
  513.                 echo -e "${RED}            Running out of room during an update can cause incomplete installations, system crashes, or render the system unbootable.${RESET}"
  514.                 echo
  515.                 read -p "Do you want to continue? [Y/n]: " response
  516.                 # Set response to 'Y' if you didn't press Y|N so Enter allows you to continue like with Yay/Pacman.
  517.                 response=${response:-Y}
  518.                 case "$response" in
  519.                     y|Y) break ;;
  520.                     n|N) exit 1 ;; # Exit script with status 1 to indicate failure.
  521.                     *) echo; echo "Please answer Y or N." ;;
  522.                 esac
  523.             done
  524.         fi
  525.     }
  526.     # WIP low-space checks. Feel free to mod.
  527.     free_space_check /efi      50    # If you use /boot/efi then change "/efi" to "/boot/efi".
  528.     free_space_check /boot     500
  529.     free_space_check /         10240
  530.     free_space_check "$HOME"   5120    # Probably not needed for updates but important to use computer.
  531.  
  532.     is_grub_installed() {
  533.         command -v grub-install &>/dev/null || find /boot /efi /EFI -type d -iname "grub" -quit &>/dev/null
  534.     }
  535.  
  536.     is_refind_installed() {
  537.         command -v refind-install &>/dev/null || find /boot /efi /EFI -type d -iname "refind" -quit &>/dev/null
  538.     }
  539.  
  540.     is_systemd_boot_installed() {
  541.         # If the bootctl command does not exist return false (1).
  542.         # This command is very likely to exist since all systems that installed the "base" arch package have systemd which installs bootctl.
  543.         command -v bootctl &>/dev/null || return 1
  544.  
  545.         # Ask bootctl if systemd-boot is installed, otherwise look for it.
  546.         sudo bootctl is-installed &>/dev/null || find /boot /efi /EFI -type d -iname "loader" -quit &>/dev/null
  547.     }
  548.  
  549.     is_pacman_hanging() {
  550.         local wait_count=0
  551.         local wait_time=7
  552.         while pgrep -x "pacman" >/dev/null || [ -f /var/lib/pacman/db.lck ]; do
  553.             case $wait_count in
  554.                 0)
  555.                     # First time don't print any warning.
  556.                     ;;
  557.                 1)
  558.                     echo
  559.                     echo -e "${RED}:: WARNING: Pacman is still running or its lock is still active. Waiting . . .${RESET}"
  560.                     wait_time=$((wait_time * 2))
  561.                     ;;
  562.                 2)
  563.                     wait_time=1
  564.                     echo
  565.                     echo -e "${RED}Pacman seems to be misbehaving. Waiting . . .${RESET}"
  566.                     echo
  567.                     echo -e "${RED}If you restart now you might find an unbootable machine due to missing or outdated initramfs and kernel modules.${RESET}"
  568.                     echo
  569.                     echo -e "${RED}Here are some steps that might help:${RESET}"
  570.                     echo
  571.                     echo -e "${RED}1. Kill the running pacman process and remove the lock:${RESET}"
  572.                     echo -e "${RED}   $ sudo killall pacman${RESET}"
  573.                     echo -e "${RED}   $ sudo rm /var/lib/pacman/db.lck${RESET}"
  574.                     echo -e "${RED}   This is necessary to run pacman again.${RESET}"
  575.                     echo
  576.                     echo -e "${RED}2. Reinstall critical packages:${RESET}"
  577.                     echo -e "${RED}   $ sudo pacman -S udev mkinitcpio linux${RESET}"
  578.                     echo -e "${RED}   $ sudo mkinitcpio -P${RESET}"
  579.                     echo -e "${RED}   This will reinstall essential packages and ensure the kernel and initramfs are created.${RESET}"
  580.                     echo
  581.                     echo -e "${RED}3. Verify package integrity:${RESET}"
  582.                     echo -e "${RED}   $ sudo pacman -Qkk${RESET}"
  583.                     echo -e "${RED}   This will check for any missing files or integrity issues with installed packages.${RESET}"
  584.                     echo
  585.                     if is_grub_installed; then
  586.                         echo -e "${RED}4. Regenerate the GRUB configuration:${RESET}"
  587.                         echo -e "${RED}   $ sudo grub-mkconfig -o /boot/grub/grub.cfg${RESET}"
  588.                     elif is_refind_installed; then
  589.                         echo -e "${RED}4. Regenerate the rEFInd configuration:${RESET}"
  590.                         echo -e "${RED}   $ sudo refind-install${RESET}"
  591.                     elif is_systemd_boot_installed; then
  592.                         echo -e "${RED}4. Regenerate the systemd-boot configuration:${RESET}"
  593.                         echo -e "${RED}   $ sudo bootctl update${RESET}"
  594.                     else
  595.                         echo -e "${RED}4. Reinstall your boot loader.${RESET}"
  596.                     fi
  597.                     echo
  598.                     echo -e "${RED}5. After performing these steps try running this script again.${RESET}"
  599.                     echo
  600.                     echo -e "${RED}Press Ctrl+C to stop waiting for pacman and exit this script.${RESET}"
  601.                     ;;
  602.             esac
  603.             sleep $wait_time
  604.             ((wait_count++))
  605.         done
  606.         if ((wait_count > 1)); then
  607.             echo -e "${GREEN}:: Pacman seems to have recovered. Continuing with the script . . .${RESET}"
  608.         fi
  609.     }
  610.  
  611.     update_arch() {
  612.         # Refresh package databases and ensure keyring is up-to-date.
  613.         # SOURCE: https://www.reddit.com/r/archlinux/comments/1leg9ds/why_doesnt_pacman_just_install_archlinuxkeyring/
  614.         is_pacman_hanging
  615.         sudo pacman -Sy --noconfirm --needed archlinux-keyring # Skips force-refresh of package databases.
  616.         # sudo pacman -Syy --noconfirm --needed archlinux-keyring # Uses -Syy to force-refresh package databases.
  617.  
  618.         # Update packages.
  619.         is_pacman_hanging
  620.         if [[ -n $DISPLAY ]]; then
  621.             # Graphical environment.
  622.             $PKG_MGR -Syu # Asks for confirmation to update packages.
  623.         else
  624.             # TTY environment.
  625.             # $PKG_MGR -Syu --noconfirm # Skip confirmation prompt and update packages.
  626.             $PKG_MGR -Syu # Asks for confirmation to update packages.
  627.         fi
  628.  
  629.         echo
  630.         echo -e "${BRIGHT_BLUE}::${RESET} Searching for *.pacnew, *.pacorig, and *.pacsave files . . ."
  631.         sudo find /etc /usr -type f \( -iname '*.pacnew' -o -iname '*.pacorig' -o -iname '*.pacsave' \) | grep --color=always -E '.*'
  632.     }
  633.     update_arch
  634.  
  635.     check_pacman_database() {
  636.         is_pacman_hanging
  637.  
  638.         echo
  639.         echo -e "${BRIGHT_BLUE}::${RESET} Checking Pacman database for issues . . ."
  640.  
  641.         local output
  642.         if output=$(sudo pacman -Dk 2>&1); then
  643.             # echo
  644.             # echo -e "${BRIGHT_BLUE}::${RESET} No Pacman database issues found . . ."
  645.             return
  646.         fi
  647.  
  648.         # If sudo privilege timeout, e.g.
  649.         #     sudo: timed out reading password
  650.         #     sudo: a password is required
  651.         if [[ "$output" == *"sudo:"* ]]; then
  652.             echo
  653.             echo -e "${BRIGHT_RED}:: ERROR: sudo authorization was lost during execution.${RESET}"
  654.             echo -e "   ${BRIGHT_RED}$output${RESET}"
  655.             exit 1
  656.         fi
  657.  
  658.         echo
  659.         echo -e "${BRIGHT_RED}Pacman database corruption detected:${RESET}"
  660.         echo
  661.         echo -e "   ${BRIGHT_RED}$output"
  662.         echo
  663.         echo -e "   ${BRIGHT_RED}Possible causes:${RESET}"
  664.         echo -e "      ${BRIGHT_RED}• System crash during a package transaction."
  665.         echo -e "      ${BRIGHT_RED}• Power loss during a package transaction."
  666.         echo -e "      ${BRIGHT_RED}• Disk corruption."
  667.         echo
  668.         echo -e "   ${BRIGHT_RED}Recommendation:${RESET}"
  669.         echo -e "      ${BRIGHT_RED}Reinstall the affected package(s)."
  670.         echo -e "      ${BRIGHT_RED}You may need to force the reinstall if files exist in the install directory:"
  671.         echo -e "         ${BRIGHT_RED}$ sudo pacman -S --overwrite \* <package_name>"
  672.  
  673.         exit 1
  674.     }
  675.     check_pacman_database
  676.  
  677.     remove_orphaned_packages() {
  678.         # Remove unneeded dependencies.
  679.         # 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.
  680.         # NOTE: The -Ycc flag specifically tells yay to clean up its own AUR build directory (usually ~/.cache/yay/) and remove "make dependencies" (packages that were only downloaded to compile an AUR package and are no longer needed).
  681.         #       Since pacman only handles pre-compiled binaries from the official repositories, it doesn't do any building, meaning it has no build cache or make dependencies to clean up.
  682.         if [[ "$PKG_MGR" == "yay" ]]; then
  683.             is_pacman_hanging
  684.             echo
  685.             echo -e "${BRIGHT_BLUE}::${RESET} Remove unneeded AUR build dependencies . . ."
  686.             $PKG_MGR -Ycc --noconfirm
  687.         fi
  688.  
  689.         # Clean Pacman cache directory /var/cache/pacman/pkg/, Pacman database directory /var/lib/pacman/, and Yay AUR build directory $HOME/.cache/yay/.
  690.         # If you need to downgrade or install an outdated package then you will need to download it again.
  691.         # NOTE: To just clean the Pacman directories the command is [$ yes | sudo pacman -Scc].
  692.         is_pacman_hanging
  693.         echo
  694.         echo -e "${BRIGHT_BLUE}::${RESET} Clean package caches . . ."
  695.  
  696.         # Had to add this because [yay -Scc] calls [pacman -Scc] under the hood.
  697.         # Pacman's cache directory normally contains files, but sometimes also directories.
  698.         # When [pacman -Scc] tries to clear the cache it doesn't use [rm -r] so you get this error:
  699.         #     error: could not remove /var/cache/pacman/pkg/download-UuJ686: Is a directory
  700.         #     error: could not remove /var/cache/pacman/pkg/download-iKYDbe: Is a directory
  701.         #     error: could not remove /var/cache/pacman/pkg/download-6MC6vA: Is a directory
  702.  
  703.         # Remove directories that pacman struggles with removing.
  704.         (
  705.             shopt -s nullglob
  706.             downloads=(/var/cache/pacman/pkg/download-*)
  707.             (( ${#downloads[@]} )) && sudo rm -rf "${downloads[@]}"
  708.         )
  709.  
  710.         yes 2>/dev/null | $PKG_MGR -Scc
  711.  
  712.         # Purge the orphans.
  713.         is_pacman_hanging
  714.         mapfile -t orphans < <(pacman -Qdtq)
  715.         if [[ ${#orphans[@]} -gt 0 ]]; then
  716.             echo
  717.             echo -e "${BRIGHT_BLUE}::${RESET} Removing orphaned packages . . ."
  718.             # sudo pacman -Rns "${orphans[@]}"
  719.             sudo pacman -Rns --noconfirm "${orphans[@]}"
  720.         else
  721.             echo
  722.             echo -e "${BRIGHT_BLUE}::${RESET} No orphaned packages found . . ."
  723.         fi
  724.     }
  725.     remove_orphaned_packages
  726.  
  727.     remove_orphaned_kernel_modules() {
  728.         # Build a map of installed kernel versions based on current /boot images.
  729.         declare -A kernel_versions
  730.         for img in /boot/vmlinuz-*; do
  731.             [[ -f "$img" ]] || continue
  732.             pkgname="${img#/boot/vmlinuz-}" # Strip '/boot/vmlinuz-' to get the kernel package name e.g. 'linux', 'linux-lts', 'linux-hardened', 'linux-zen' etc.
  733.  
  734.             # If a package named $pkgname is installed.
  735.             if pacman -Q "$pkgname" &>/dev/null; then
  736.                 # Add an associative array entry to map a kernel package to its installed version.
  737.                 kernel_versions["$pkgname"]="$(pacman -Q "$pkgname" | awk '{print $2}')"
  738.             fi
  739.         done
  740.  
  741.         # Collect orphaned module directories.
  742.         local orphans=()
  743.         for dir in /usr/lib/modules/*/; do # The trailing '/' ensures only directories are matched.
  744.             [[ -d "$dir" ]] || continue # Skip if this isn't a directory.
  745.             module_dir=$(basename "$dir")
  746.  
  747.             skip=false
  748.             for version in "${kernel_versions[@]}"; do
  749.                 [[ "$module_dir" == "$version" ]] && { skip=true; break; }
  750.             done
  751.             [[ $skip == true ]] && continue # Skip if this module's kernel is installed.
  752.  
  753.             # If no installed package owns this module directory.
  754.             if ! pacman -Qo "/usr/lib/modules/${module_dir}" &>/dev/null; then
  755.                 orphans+=("${module_dir}") # Mark this module directory as orphaned.
  756.             fi
  757.         done
  758.  
  759.         # If no orphaned kernel modules found.
  760.         if [[ ${#orphans[@]} -eq 0 ]]; then
  761.             echo
  762.             echo -e "${BRIGHT_BLUE}::${RESET} No orphaned kernel modules found . . ."
  763.             return
  764.         fi
  765.  
  766.         echo
  767.         echo -e "${BRIGHT_BLUE}::${RESET} Remove orphaned kernel modules . . ."
  768.  
  769.         # Print installed kernels.
  770.         echo
  771.         echo -e "   ${BRIGHT_CYAN}Installed kernels:${RESET}"
  772.         for pkgname in "${!kernel_versions[@]}"; do
  773.             echo -e "       ${BRIGHT_CYAN}${pkgname}: ${kernel_versions[$pkgname]}${RESET}"
  774.         done
  775.  
  776.         # Print orphaned modules.
  777.         echo
  778.         echo -e "   ${BRIGHT_RED}Orphaned kernel modules:${RESET}"
  779.         for orphan in "${orphans[@]}"; do
  780.             echo -e "       ${BRIGHT_RED}\"/usr/lib/modules/${orphan}\"${RESET}"
  781.         done
  782.  
  783.         # Read DKMS status once and collect a list of unique module names.
  784.         local dkms_lines=()
  785.         local modules=()
  786.         if command -v dkms &>/dev/null; then
  787.             mapfile -t dkms_lines < <(dkms status)
  788.             mapfile -t modules < <(printf '%s\n' "${dkms_lines[@]}" | awk -F',' '{print $1}' | sort -u)
  789.         fi
  790.  
  791.         # Remove orphaned kernel modules.
  792.         local remove_all=false
  793.         for orphan in "${orphans[@]}"; do
  794.             echo
  795.             echo -e "   ${BRIGHT_YELLOW}Remove \"/usr/lib/modules/${orphan}\"${RESET}"
  796.  
  797.             # Show DKMS modules associated with this orphan.
  798.             matched_modules=()
  799.             if [[ ${#modules[@]} -gt 0 ]]; then
  800.                 # Loop through each unique module.
  801.                 for mod in "${modules[@]}"; do
  802.                     for line in "${dkms_lines[@]}"; do
  803.                         # If module is built for the orphaned kernel.
  804.                         if [[ "$line" == "${mod}, ${orphan},"* ]]; then
  805.                             matched_modules+=("$mod")
  806.                             break
  807.                         fi
  808.                     done
  809.                 done
  810.  
  811.                 # If DKMS modules exist which are associated with the orphaned kernel.
  812.                 if [[ ${#matched_modules[@]} -gt 0 ]]; then
  813.                     echo
  814.                     echo -e "   ${BRIGHT_YELLOW}Remove DKMS modules associated with '${orphan}':${RESET}"
  815.                     for mod in "${matched_modules[@]}"; do
  816.                         echo -e "       ${BRIGHT_YELLOW}${mod}${RESET}"
  817.                     done
  818.                 fi
  819.             fi
  820.  
  821.             # Ask the user if they want to delete or skip.
  822.             if [[ $remove_all == false ]]; then
  823.                 while true; do
  824.                     echo
  825.                     read -p "   Do you want to continue? [Y/N/A]: " response
  826.                     case "$response" in
  827.                         y|Y) break ;; # Remove this orphan.
  828.                         n|N) continue 2 ;; # Skip to next orphan.
  829.                         a|A) remove_all=true; break ;; # Remove all orphans.
  830.                         *) echo; echo "   Please answer Y, N, or A." ;;
  831.                     esac
  832.                 done
  833.             fi
  834.  
  835.             sudo rm -rf "/usr/lib/modules/${orphan}"
  836.  
  837.             # Remove DKMS records.
  838.             if [[ ${#matched_modules[@]} -gt 0 ]]; then
  839.                 for mod in "${matched_modules[@]}"; do
  840.                     echo -e "   ${BRIGHT_CYAN}Removing DKMS record for '${mod}' on '${orphan}'${RESET}"
  841.                     sudo dkms remove "${mod}" -k "${orphan}" --all
  842.                 done
  843.             fi
  844.         done
  845.  
  846.         echo
  847.         echo -e "   ${BRIGHT_GREEN}Orphaned kernel module cleanup complete . . .${RESET}"
  848.     }
  849.     remove_orphaned_kernel_modules
  850.  
  851.     is_system_bootable() {
  852.         # Find kernel images.
  853.         # local kernel_images=($(ls /boot/vmlinuz-* 2>/dev/null))
  854.         local kernel_images=($(shopt -s nullglob; echo /boot/vmlinuz-*))
  855.         if [[ ${#kernel_images[@]} -eq 0 ]]; then
  856.             echo
  857.             echo -e "${MAGENTA}:: WARNING: No kernel images found in /boot${RESET}"
  858.             exit 1
  859.         fi
  860.  
  861.         # Create a list of expected initramfs files based on the list of detected kernels.
  862.         local initramfs_images=()
  863.         for kernel in "${kernel_images[@]}"; do
  864.             local base_name="${kernel#/boot/vmlinuz-}"
  865.             initramfs_images+=("/boot/initramfs-${base_name}.img")
  866.             # As of version 40 of mkinitcpio, fallback initramfs generation is disabled by default: https://wiki.archlinux.org/title/Mkinitcpio
  867.             # initramfs_images+=("/boot/initramfs-${base_name}-fallback.img")
  868.         done
  869.  
  870.         # Check for missing kernel and initramfs files.
  871.         local missing_files=()
  872.         for file in "${kernel_images[@]}" "${initramfs_images[@]}"; do
  873.             [[ ! -f "$file" ]] && missing_files+=("$file")
  874.         done
  875.  
  876.         # Check for CPU-specific microcode image.
  877.         local expected_ucode=""
  878.         if grep -q "GenuineIntel" /proc/cpuinfo; then
  879.             expected_ucode="/boot/intel-ucode.img"
  880.         elif grep -q "AuthenticAMD" /proc/cpuinfo; then
  881.             expected_ucode="/boot/amd-ucode.img"
  882.         fi
  883.  
  884.         # If a known CPU is detected, verify its microcode file exists.
  885.         if [[ -n "$expected_ucode" && ! -f "$expected_ucode" ]]; then
  886.             missing_files+=("$expected_ucode")
  887.         fi
  888.  
  889.         # Check for missing bootloader files.
  890.         if is_grub_installed; then
  891.             local grub_cfg
  892.             grub_cfg=$(find /boot /efi /EFI -type f -path "*/grub/grub.cfg" -print -quit 2>/dev/null)
  893.             [[ -z "$grub_cfg" ]] && missing_files+=("esp/grub.cfg")
  894.         elif is_refind_installed; then
  895.             local refind_conf
  896.             refind_conf=$(find /boot /efi /EFI -type f -path "*/refind/refind.conf" -print -quit 2>/dev/null)
  897.             [[ -z "$refind_conf" ]] && missing_files+=("esp/refind/refind.conf")
  898.         elif is_systemd_boot_installed; then
  899.             local loader_conf
  900.             loader_conf=$(find /boot /efi /EFI -type f -path "*/loader/loader.conf" -print -quit 2>/dev/null)
  901.             [[ -z "$loader_conf" ]] && missing_files+=("esp/loader/loader.conf")
  902.             if ! find /boot /efi /EFI -type f -path "*/loader/entries/*.conf" -quit &>/dev/null; then
  903.                 missing_files+=("esp/loader/entries/*.conf")
  904.             fi
  905.         fi
  906.  
  907.         if [[ ${#missing_files[@]} -ne 0 ]]; then
  908.             echo
  909.             echo -e "${MAGENTA}:: WARNING: System boot files are missing:${RESET}"
  910.             for file in "${missing_files[@]}"; do
  911.                 echo -e "${MAGENTA}   $file${RESET}"
  912.             done
  913.             echo
  914.             echo -e "${BOLD}${GREEN}   Suggested repairs:${RESET}"
  915.             echo
  916.             echo -e "${BOLD}${GREEN}   $ sudo mkinitcpio -P${RESET}"
  917.             echo
  918.             if is_grub_installed; then
  919.                 [[ -z "$grub_cfg" ]] && echo -e "${BOLD}${GREEN}   $ sudo grub-mkconfig -o /boot/grub/grub.cfg${RESET}" && echo
  920.             elif is_refind_installed; then
  921.                 [[ -z "$refind_conf" ]] && echo -e "${BOLD}${GREEN}   $ sudo refind-install${RESET}" && echo
  922.             elif is_systemd_boot_installed; then
  923.                 [[ -z "$loader_conf" ]] && echo -e "${BOLD}${GREEN}   $ sudo bootctl update${RESET}" && echo
  924.             else
  925.                 echo -e "${BOLD}${GREEN}   Reinstall your boot loader.${RESET}" && echo
  926.             fi
  927.             exit 1
  928.         fi
  929.  
  930.         echo
  931.         echo -e "${BRIGHT_BLUE}::${RESET} No boot issues detected . . ."
  932.     }
  933.     is_system_bootable
  934.  
  935.     check_ssd_trim_timer() {
  936.         if ! systemctl is-active --quiet fstrim.timer; then
  937.             echo
  938.             echo -e "${RED}::${RESET} WARNING: fstrim.timer not active! SSD trim will not run automatically . . ."
  939.             echo -e "${RED}::${RESET}          Run $ sudo systemctl enable fstrim.timer --now"
  940.         else
  941.             echo
  942.             echo -e "${BRIGHT_BLUE}::${RESET} SSD trim timer is active . . ."
  943.         fi
  944.     }
  945.     check_ssd_trim_timer
  946.  
  947. File: /usr/local/bin/yap
  948. USAGE: $ yap    <-- Show recent Arch Linux news events, highlighting events since the last system upgrade.
  949.  
  950.     #!/usr/bin/env python3
  951.  
  952.     # Installation:
  953.     #     $ sudo cp ./yap /usr/local/bin/
  954.     #     $ sudo chmod +x /usr/local/bin/yap
  955.  
  956.     import os
  957.  
  958.     # ----- Colors -----
  959.     BLACK = '\033[30m'
  960.     RED = '\033[31m'
  961.     GREEN = '\033[32m'
  962.     YELLOW = '\033[33m'
  963.     BLUE = '\033[34m'
  964.     MAGENTA = '\033[35m'
  965.     CYAN = '\033[36m'
  966.     WHITE = '\033[37m'
  967.     BRIGHT_BLACK = '\033[90m'
  968.     BRIGHT_RED = '\033[91m'
  969.     BRIGHT_GREEN = '\033[92m'
  970.     BRIGHT_YELLOW = '\033[93m'
  971.     BRIGHT_BLUE = '\033[94m'
  972.     BRIGHT_MAGENTA = '\033[95m'
  973.     BRIGHT_CYAN = '\033[96m'
  974.     BRIGHT_WHITE = '\033[97m'
  975.  
  976.     # ----- Styles -----
  977.     RESET = '\033[0m' # Reset all styles.
  978.     BOLD = '\033[1m'
  979.     HALF_BRIGHT = '\033[2m'
  980.     ITALIC = '\033[3m'
  981.     UNDERLINE = '\033[4m'
  982.     REVERSED = '\033[7m' # Reverse foreground and background colors.
  983.  
  984.     import html
  985.     import re
  986.     import shutil
  987.     import subprocess
  988.     import sys
  989.     import time
  990.     import xml.etree.ElementTree as ET
  991.     from datetime import datetime
  992.     from urllib.error import URLError, HTTPError
  993.     from urllib.request import urlopen, Request
  994.  
  995.     NEWS_FEED_URL = "https://archlinux.org/feeds/news/"
  996.     PACMAN_LOG = "/var/log/pacman.log"
  997.  
  998.     import argparse
  999.     def usage():
  1000.         parser = argparse.ArgumentParser(
  1001.             description="Show recent Arch Linux news events, highlighting ones since your last system upgrade.",
  1002.             add_help=False
  1003.         )
  1004.         parser.add_argument("--brief", action="store_true", help="Show titles and URLs only (omit descriptions).")
  1005.         parser.add_argument("--current", action="store_true", help="Only show news since your last full upgrade.")
  1006.         parser.add_argument("--max", type=int, default=0, help="Limit number of news items shown.")
  1007.         parser.add_argument("--debug", action="store_true", help="Force all news to appear new (for testing).")
  1008.         parser.add_argument("--help", "-h", action="help", help="Show this help message and exit.")
  1009.  
  1010.         try:
  1011.             global args
  1012.             args = parser.parse_args()
  1013.         except SystemExit:
  1014.             sys.exit(0) # No news.
  1015.  
  1016.     def get_last_upgrade_time(debug=False):
  1017.         if debug:
  1018.             return 0
  1019.  
  1020.         try:
  1021.             with open(PACMAN_LOG, "r") as f:
  1022.                 for line in reversed(f.readlines()):
  1023.                     if "starting full system upgrade" in line:
  1024.                         ts_match = re.search(r"\[(.*?)\]", line)
  1025.                         if ts_match:
  1026.                             ts_str = ts_match.group(1)
  1027.                             return int(datetime.strptime(ts_str, "%Y-%m-%dT%H:%M:%S%z").timestamp())
  1028.         except Exception:
  1029.             pass
  1030.  
  1031.         return 0 # Fallback if no match.
  1032.  
  1033.     def fetch_rss_feed(url):
  1034.         try:
  1035.             req = Request(url, headers={"User-Agent": "Mozilla/5.0"})
  1036.             with urlopen(req, timeout=5) as response:
  1037.                 return response.read().decode("utf-8")
  1038.         except HTTPError as e:
  1039.             print(f"{BRIGHT_RED}HTTP Error {e.code}: {e.reason}{RESET}", file=sys.stderr)
  1040.         except URLError as e:
  1041.             print(f"{BRIGHT_RED}URL Error: {e.reason}{RESET}", file=sys.stderr)
  1042.         return None
  1043.  
  1044.     def simplify_links(text):
  1045.         #  # Convert <a href="url">text</a> to [text](url)
  1046.         #  text = re.sub(r'<a[^>]+href=["\']([^"\']+)["\'][^>]*>(.*?)</a>', r'[\2](\1)', text)
  1047.         #  # Simplify [text](url) to just text if text == url
  1048.         #  text = re.sub(r'\[([^\]]+)\]\(([^)]+)\)', lambda m: m.group(1) if m.group(1) == m.group(2) else m.group(0), text)
  1049.  
  1050.         # Convert <a href="url">text</a> to [text](url), or just text if text == url.
  1051.         text = re.sub(
  1052.             r'<a[^>]+href=["\']([^"\']+)["\'][^>]*>(.*?)</a>',
  1053.            lambda m: m.group(1) if m.group(1) == m.group(2) else f'[{m.group(2)}]({m.group(1)})',
  1054.            text
  1055.        )
  1056.  
  1057.        # Unescape HTML entities recursively.
  1058.        prev = None
  1059.        while prev != text:
  1060.            prev = text
  1061.            text = html.unescape(text)
  1062.  
  1063.        # Strip remaining HTML tags.
  1064.        text = re.sub(r"<[^>]+>", "", text)
  1065.  
  1066.        return text.strip()
  1067.  
  1068.    def parse_feed(xml_data):
  1069.        root = ET.fromstring(xml_data)
  1070.        channel = root.find("channel")
  1071.        return channel.findall("item") if channel is not None else []
  1072.  
  1073.    def day_label(target_ts, now_ts=None):
  1074.        """
  1075.        Return TODAY, YESTERDAY, TOMORROW, or D-/D+ labels.
  1076.        Mirrors the behavior of the yow script.
  1077.        """
  1078.        if now_ts is None:
  1079.            # Midnight today
  1080.            today = datetime.now().strftime("%Y-%m-%d")
  1081.            now_ts = int(datetime.strptime(today, "%Y-%m-%d").timestamp())
  1082.  
  1083.        # Convert target timestamp to midnight of its date
  1084.        target_date = datetime.fromtimestamp(target_ts).strftime("%Y-%m-%d")
  1085.        target_midnight = int(datetime.strptime(target_date, "%Y-%m-%d").timestamp())
  1086.  
  1087.        # Full-day difference (floor division)
  1088.        days_diff = (target_midnight - now_ts) // 86400
  1089.  
  1090.        if days_diff == 0:
  1091.            return "TODAY"
  1092.        elif days_diff == -1:
  1093.            return "YESTERDAY"
  1094.        elif days_diff == 1:
  1095.            return "TOMORROW"
  1096.        elif days_diff < 0:
  1097.            return f"D{days_diff}"
  1098.        else:
  1099.            return f"D+{days_diff}"
  1100.  
  1101.    def display_news(items, last_upgrade, brief=False, current_only=False, max_events=0):
  1102.        now = int(time.time())
  1103.        count = 0
  1104.  
  1105.        for item in items:
  1106.            title = simplify_links(item.findtext("title", default=""))
  1107.            link = simplify_links(item.findtext("link", default=""))
  1108.            desc = simplify_links(item.findtext("description", default=""))
  1109.            pub_date = item.findtext("pubDate", default="")
  1110.  
  1111.            try:
  1112.                pub_ts = int(datetime.strptime(pub_date, "%a, %d %b %Y %H:%M:%S %z").timestamp())
  1113.            except Exception:
  1114.                continue
  1115.  
  1116.            if current_only and pub_ts <= last_upgrade:
  1117.                continue
  1118.  
  1119.            color = BRIGHT_RED if pub_ts > last_upgrade else BRIGHT_GREEN
  1120.            status = "[NEW] " if pub_ts > last_upgrade else ""
  1121.            label = day_label(pub_ts, now)
  1122.            iso_date = datetime.fromtimestamp(pub_ts).strftime("%Y-%m-%d")
  1123.  
  1124.            print(f"\n{color}{status}{label}/{iso_date} {title}{RESET}")
  1125.            print(f"{color}@ {RESET}{link}")
  1126.            if not brief:
  1127.                print(f"{color}>{RESET} {desc}")
  1128.  
  1129.            count += 1
  1130.            if max_events > 0 and count >= max_events:
  1131.                break
  1132.  
  1133.        return count > 0
  1134.  
  1135.    def main():
  1136.        usage()
  1137.  
  1138.        # Paging support.
  1139.        if sys.stdout.isatty() and "YAP_IN_LESS" not in os.environ and len(sys.argv) == 1:
  1140.            os.environ["YAP_IN_LESS"] = "1"
  1141.            if shutil.which("less"):
  1142.                pager = ["less", "-R"]
  1143.                shell = False
  1144.            else:
  1145.                pager = None
  1146.                shell = False
  1147.  
  1148.            if pager:
  1149.                proc = subprocess.Popen([sys.executable] + sys.argv, stdout=subprocess.PIPE)
  1150.                try:
  1151.                    subprocess.run(pager, stdin=proc.stdout, shell=shell)
  1152.                except KeyboardInterrupt:
  1153.                    pass
  1154.  
  1155.                if sys.platform != 'win32':
  1156.                    proc.wait()
  1157.  
  1158.                    code = proc.returncode
  1159.                    if code in (0, 2): # Preserve 0 (no news) / 1 (error) / 2 (news) codes.
  1160.                        sys.exit(code)
  1161.                    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).
  1162.                        sys.exit(1) # Error.
  1163.                else:
  1164.                    sys.exit(0) # No news.
  1165.  
  1166.        # Fetch feed and display.
  1167.        feed = fetch_rss_feed(NEWS_FEED_URL)
  1168.        if feed is None:
  1169.            sys.exit(1) # Error.
  1170.  
  1171.        last_upgrade = 0 if args.debug else get_last_upgrade_time()
  1172.        upgrade_iso = datetime.fromtimestamp(last_upgrade).isoformat()
  1173.        label = day_label(last_upgrade)
  1174.  
  1175.        print(f"{BRIGHT_GREEN}Last full system upgrade:{RESET} {label}/{upgrade_iso}")
  1176.  
  1177.        items = parse_feed(feed)
  1178.        try:
  1179.            success = display_news(items, last_upgrade, brief=args.brief, current_only=args.current, max_events=args.max)
  1180.        except BrokenPipeError:
  1181.            try:
  1182.                sys.stdout.close()
  1183.            except Exception:
  1184.                pass
  1185.            finally:
  1186.                sys.exit(0) # No news. The user quit by pressing Q.
  1187.  
  1188.        if success:
  1189.            sys.exit(2) # News.
  1190.        else:
  1191.            if args.current:
  1192.                print("No news detected since last upgrade")
  1193.            else:
  1194.                print("No news items found in the feed")
  1195.            sys.exit(0) # No news.
  1196.  
  1197.    if __name__ == "__main__":
  1198.        main()
  1199.  
  1200. File: /usr/local/bin/yow
  1201. USAGE: $ yow    <-- Show a colorized version of /var/log/pacman.log.
  1202.  
  1203.    #!/bin/bash
  1204.  
  1205.    # Installation:
  1206.    #     $ sudo cp ./yow /usr/local/bin/
  1207.    #     $ sudo chmod +x /usr/local/bin/yow
  1208.  
  1209.    # ----- Colors -----
  1210.    BLACK="\033[30m"
  1211.    RED="\033[31m"
  1212.    GREEN="\033[32m"
  1213.    YELLOW="\033[33m"
  1214.    BLUE="\033[34m"
  1215.    MAGENTA="\033[35m"
  1216.    CYAN="\033[36m"
  1217.    WHITE="\033[37m"
  1218.    BRIGHT_BLACK="\033[90m"
  1219.    BRIGHT_RED="\033[91m"
  1220.    BRIGHT_GREEN="\033[92m"
  1221.    BRIGHT_YELLOW="\033[93m"
  1222.    BRIGHT_BLUE="\033[94m"
  1223.    BRIGHT_MAGENTA="\033[95m"
  1224.    BRIGHT_CYAN="\033[96m"
  1225.    BRIGHT_WHITE="\033[97m"
  1226.  
  1227.    # ----- Styles -----
  1228.    RESET="\033[0m" # Reset all styles.
  1229.    BOLD="\033[1m"
  1230.    HALF_BRIGHT="\033[2m"
  1231.    ITALIC="\033[3m"
  1232.    UNDERLINE="\033[4m"
  1233.    REVERSED="\033[7m" # Reverse foreground/background.
  1234.  
  1235.    PACMAN_LOG='/var/log/pacman.log'
  1236.  
  1237.    package_name=""
  1238.    mode=""
  1239.    date_value=""
  1240.    show_all=false
  1241.    no_color=false
  1242.  
  1243.    # ----- Members -----
  1244.    display_help() {
  1245.        cat <<EOF
  1246.    Display and colorize $PACMAN_LOG entries.
  1247.  
  1248.    USAGE: $ yow [OPTION]
  1249.  
  1250.    NOTE: If no option is provided then today's $(date +%Y-%m-%d) pacman.log entries will be displayed.
  1251.  
  1252.     Options:
  1253.         --all                    Show the entire pacman log colorized.
  1254.         --no-color               Skip colorizing output.
  1255.  
  1256.         --date=YYYY-MM-DD        Show entries for the given date.
  1257.         --day=N                  Show entries for N days ago.
  1258.         --day=today|yesterday    Show entries for the named day.
  1259.  
  1260.         --since=YYYY-MM-DD       Show entries starting from the given date.
  1261.         --since=N                Show entries starting from N days ago.
  1262.         --since=today|yesterday  Show entries starting from the named day.
  1263.  
  1264.         --today                  Alias for --day=today.
  1265.         --yesterday              Alias for --day=yesterday.
  1266.  
  1267.         --related=package        Only show entries related to the package and its dependencies.
  1268.  
  1269.         -h, --help               Show this help message and exit.
  1270.     EOF
  1271.     }
  1272.  
  1273.     colorize_line() {
  1274.         local line="$1"
  1275.  
  1276.         # Skip colorizing output.
  1277.         if [[ "$no_color" == true ]]; then
  1278.             echo "$line"
  1279.             return
  1280.         fi
  1281.  
  1282.         case "$line" in
  1283.             *"error:"*|*"warning:"*)
  1284.                 echo -e "${BRIGHT_YELLOW}${line}${RESET}" ;;
  1285.             *"[ALPM-SCRIPTLET]"*|*".pacnew"*|*".pacsave"*|*".pacorig"*)
  1286.                 echo -e "${BRIGHT_BLACK}${line}${RESET}" ;;
  1287.             *"[ALPM] installed "*|*"[ALPM] upgraded "*)
  1288.                 echo -e "${BRIGHT_CYAN}${line}${RESET}" ;;
  1289.             *"[ALPM] removed "*)
  1290.                 echo -e "${BRIGHT_RED}${line}${RESET}" ;;
  1291.             *"[ALPM] downgraded "*)
  1292.                 echo -e "${BRIGHT_MAGENTA}${line}${RESET}" ;;
  1293.             *)
  1294.                 echo -e "$line" ;;
  1295.         esac
  1296.     }
  1297.  
  1298.     # ----- Date handling -----
  1299.     normalize_date() {
  1300.         local input="$1"
  1301.         date -d "$input" +%Y-%m-%d 2>/dev/null || return 1
  1302.     }
  1303.  
  1304.     resolve_date_arg() {
  1305.         local value="$1"
  1306.         case "$value" in
  1307.             today) date +%Y-%m-%d ;;
  1308.             yesterday) date -d "yesterday" +%Y-%m-%d ;;
  1309.             ''|*[!0-9]*) date -d "$value" +%Y-%m-%d 2>/dev/null ;; # full date string
  1310.             *) date -d "$value days ago" +%Y-%m-%d ;; # numeric days ago
  1311.         esac
  1312.     }
  1313.  
  1314.     create_date_label() {
  1315.         local log_date="$1"
  1316.         local now_ts log_ts days_diff
  1317.  
  1318.         # Convert log_date to seconds since epoch (midnight of that day)
  1319.         log_ts=$(date -d "$log_date" +%s 2>/dev/null) || return
  1320.  
  1321.         # Use midnight of today for comparison
  1322.         now_ts=$(date -d "$(date +%Y-%m-%d)" +%s)
  1323.  
  1324.         # Calculate difference in full days
  1325.         days_diff=$(( (log_ts - now_ts) / 86400 ))
  1326.  
  1327.         case $days_diff in
  1328.             -1) echo "YESTERDAY/" ;;
  1329.             0)  echo "TODAY/" ;;
  1330.             1)  echo "TOMORROW/" ;;
  1331.             *)
  1332.                 if (( days_diff < 0 )); then
  1333.                     echo "D${days_diff}/" # Negative days as D-3, D-4, etc.
  1334.                 else
  1335.                     echo "D+${days_diff}/" # Future days.
  1336.                 fi ;;
  1337.         esac
  1338.     }
  1339.  
  1340.     create_dependency_regex() {
  1341.         local pkg="$1"
  1342.         if [[ -z "$pkg" ]]; then
  1343.             echo "" # Return "".
  1344.             return 0
  1345.         fi
  1346.  
  1347.         if ! pacman -Q "$pkg" &>/dev/null; then
  1348.             echo "ERROR: Package '$pkg' is not installed." >&2
  1349.             exit 1
  1350.         fi
  1351.  
  1352.         # Get dependencies.
  1353.         local pkgs=$(pactree -luo "$pkg" 2>/dev/null || true)
  1354.  
  1355.         # Include the main package.
  1356.         pkgs="$pkgs"$'\n'"$pkg"
  1357.  
  1358.         # Sanitize the pactree output.
  1359.         # Strip any version constraints (>=, =, <, > etc.), e.g. convert "glibc>=2.27" to "glibc".
  1360.         # Remove empty lines.
  1361.         pkgs=$(echo "$pkgs" | sed -E 's/[<>=].*$//' | sed '/^$/d')
  1362.  
  1363.         # Build a grep -E compatible regex for whole package name matching.
  1364.         # It only matches if the package name is surrounded by characters that are NOT alphanumeric, '_', '.', or '-'.
  1365.         # NOTE: I tried [echo "\b($(echo "$pkgs" | paste -sd'|' -))\b"] but it was too permissive.
  1366.         #       '-' was treated as divider so a package like 'libxkbcommon' would also match 'lib32-libxkbcommon'.
  1367.         echo "(^|[^[:alnum:]_.-])($(echo "$pkgs" | paste -sd'|' -))($|[^[:alnum:]_.-])"
  1368.     }
  1369.  
  1370.     # ----- Log display -----
  1371.     print_header() {
  1372.         local text="$1"
  1373.         if [[ "$no_color" == true ]]; then
  1374.             echo ":: $text"
  1375.         else
  1376.             echo -e "${BRIGHT_BLUE}::${RESET} $text"
  1377.         fi
  1378.     }
  1379.  
  1380.     display_pacman_log() {
  1381.         local log_date="$1"
  1382.         local mode="${2:-exact}"
  1383.  
  1384.         if [[ ! -s "$PACMAN_LOG" ]]; then
  1385.             echo "ERROR: Log is empty or unreadable: $PACMAN_LOG" >&2
  1386.             exit 1
  1387.         fi
  1388.  
  1389.         local dependency_regex=$(create_dependency_regex "$package_name")
  1390.  
  1391.         local related_suffix=""
  1392.         [[ -n "$package_name" ]] && related_suffix=" (related to $package_name)"
  1393.  
  1394.         # Since-date mode.
  1395.         if [[ "$mode" == "since" ]]; then
  1396.             local current_date="$log_date"
  1397.             local today=$(date +%Y-%m-%d)
  1398.             local end_date=$(date -d "$today +1 day" +%Y-%m-%d)
  1399.  
  1400.             while [[ "$current_date" < "$end_date" ]]; do
  1401.                 # Get all entries for the date.
  1402.                 local matches
  1403.                 matches=$(grep -F -- "$current_date" "$PACMAN_LOG" 2>/dev/null || true)
  1404.  
  1405.                 # Apply package filter first if requested.
  1406.                 if [[ -n "$dependency_regex" && -n "$matches" ]]; then
  1407.                     matches=$(echo "$matches" | grep -E "$dependency_regex")
  1408.                 fi
  1409.  
  1410.                 # Only print header if matches exist.
  1411.                 if [[ -n "$matches" ]]; then
  1412.                     local date_label=$(create_date_label "$current_date")
  1413.                     print_header "Viewing $PACMAN_LOG entries for ${date_label}${current_date}${related_suffix}"
  1414.                     echo "$matches"
  1415.                     echo
  1416.                 fi
  1417.  
  1418.                 current_date=$(date -d "$current_date +1 day" +%Y-%m-%d)
  1419.             done | while IFS= read -r line; do
  1420.                 colorize_line "$line"
  1421.             done | less -R
  1422.  
  1423.         # Exact-date mode.
  1424.         else
  1425.             local matches=$(grep -F -- "$log_date" "$PACMAN_LOG" 2>/dev/null || true)
  1426.  
  1427.             if [[ -n "$dependency_regex" && -n "$matches" ]]; then
  1428.                 matches=$(echo "$matches" | grep -E "$dependency_regex")
  1429.             fi
  1430.  
  1431.             if [[ -z "$matches" ]]; then
  1432.                 local label=$(create_date_label "$log_date")
  1433.                 print_header "No entries found for ${label}${log_date}${related_suffix} in $PACMAN_LOG"
  1434.                 exit 0
  1435.             fi
  1436.  
  1437.             local label=$(create_date_label "$log_date")
  1438.  
  1439.             {
  1440.                 print_header "Viewing $PACMAN_LOG entries for ${label}${log_date}${related_suffix}"
  1441.                 echo "$matches"
  1442.             } | while IFS= read -r line; do
  1443.                 colorize_line "$line"
  1444.             done | less -R
  1445.         fi
  1446.     }
  1447.  
  1448.     # ----- Argument parsing -----
  1449.     for arg in "$@"; do
  1450.         case "$arg" in
  1451.             -h|--help)
  1452.                 display_help
  1453.                 exit 0 ;;
  1454.             --related=*)
  1455.                 package_name="${arg#--related=}" ;;
  1456.             --all)
  1457.                 show_all=true ;;
  1458.             --no-color)
  1459.                 no_color=true ;;
  1460.             --today)
  1461.                 mode="exact"
  1462.                 date_value="$(date +%Y-%m-%d)" ;;
  1463.             --yesterday)
  1464.                 mode="exact"
  1465.                 date_value="$(date -d yesterday +%Y-%m-%d)" ;;
  1466.             --date=*)
  1467.                 mode="exact"
  1468.                 date_value="${arg#--date=}" ;;
  1469.             --since=*)
  1470.                 mode="since"
  1471.                 date_value="$(resolve_date_arg "${arg#--since=}")" ;;
  1472.             --day=*)
  1473.                 mode="exact"
  1474.                 date_value="$(resolve_date_arg "${arg#--day=}")" ;;
  1475.             *)
  1476.                 echo "ERROR: Unknown option '$arg'" >&2
  1477.                 exit 1 ;;
  1478.         esac
  1479.     done
  1480.  
  1481.     # Check for conflicting options
  1482.     if [[ "$show_all" == true && -n "$date_value" ]]; then
  1483.         echo "ERROR: Cannot use --all with specific date options." >&2
  1484.         exit 1
  1485.     fi
  1486.  
  1487.     # Defaults
  1488.     if [[ -z "$date_value" && "$show_all" != true ]]; then
  1489.         mode="exact"
  1490.         date_value="$(date +%Y-%m-%d)"
  1491.     fi
  1492.  
  1493.     # Dispatch
  1494.     if [[ "$show_all" == true ]]; then
  1495.         first_date=$(grep -o '^\[[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}' "$PACMAN_LOG" | head -n1 | tr -d '[')
  1496.         if ! norm=$(normalize_date "$first_date"); then
  1497.             echo "ERROR: Could not determine start date of $PACMAN_LOG" >&2
  1498.             exit 1
  1499.         fi
  1500.         display_pacman_log "$norm" since
  1501.     else
  1502.         if ! norm=$(normalize_date "$date_value"); then
  1503.             echo "ERROR: Invalid date format: '$date_value'" >&2
  1504.             exit 1
  1505.         fi
  1506.         display_pacman_log "$norm" "$mode"
  1507.     fi
  1508.  
  1509. File: /usr/local/bin/yup
  1510. USAGE: $ yup    <-- Updates my vim/nvim plugins.
  1511.  
  1512.     #!/bin/bash
  1513.  
  1514.     # Installation:
  1515.     #     $ sudo cp ./yup /usr/local/bin/
  1516.     #     $ sudo chmod +x /usr/local/bin/yup
  1517.  
  1518.     # ----- Colors -----
  1519.     BLACK="\033[30m"
  1520.     RED="\033[31m"
  1521.     GREEN="\033[32m"
  1522.     YELLOW="\033[33m"
  1523.     BLUE="\033[34m"
  1524.     MAGENTA="\033[35m"
  1525.     CYAN="\033[36m"
  1526.     WHITE="\033[37m"
  1527.     BRIGHT_BLACK="\033[90m"
  1528.     BRIGHT_RED="\033[91m"
  1529.     BRIGHT_GREEN="\033[92m"
  1530.     BRIGHT_YELLOW="\033[93m"
  1531.     BRIGHT_BLUE="\033[94m"
  1532.     BRIGHT_MAGENTA="\033[95m"
  1533.     BRIGHT_CYAN="\033[96m"
  1534.     BRIGHT_WHITE="\033[97m"
  1535.  
  1536.     # ----- Styles -----
  1537.     RESET="\033[0m" # Reset all styles.
  1538.     BOLD="\033[1m"
  1539.     HALF_BRIGHT="\033[2m"
  1540.     ITALIC="\033[3m"
  1541.     UNDERLINE="\033[4m"
  1542.     REVERSED="\033[7m" # Reverse foreground/background.
  1543.  
  1544.     # Prompt for sudo at the start.
  1545.     if ! sudo -v; then
  1546.         echo -e "${BRIGHT_BLUE}::${RESET} This script requires sudo privileges. Exiting."
  1547.         exit 1
  1548.     fi
  1549.  
  1550.     update_vim_plugins() {
  1551.         # ----- Vim -----
  1552.         if command -v vim &>/dev/null && [ -f ~/.vim/autoload/plug.vim ]; then
  1553.             echo -e "${BRIGHT_BLUE}::${RESET} Updating user vim plugins . . ."
  1554.             # Update ~/.vim/ plugins.
  1555.             vim -es +'silent! PlugUpgrade' +'silent! PlugUpdate' +qall
  1556.             echo
  1557.  
  1558.             if sudo -n true 2>/dev/null; then
  1559.                 echo -e "${BRIGHT_BLUE}::${RESET} Updating root vim plugins . . ."
  1560.                 # Update /root/.vim/ plugins.
  1561.                 # -E is needed so the environment variables are available to the sudo instance of vim.
  1562.                 # -H is needed so $HOME=/root instead of ~/home/wolf (a side effect of using the -E option).
  1563.                 sudo -EH vim -es +'silent! PlugUpgrade' +'silent! PlugUpdate' +qall
  1564.             else
  1565.                 echo -e "${BRIGHT_BLUE}::${RESET} Skipping root vim plugins (no sudo)."
  1566.             fi
  1567.         fi
  1568.  
  1569.         # ----- Neovim -----
  1570.         if command -v nvim &>/dev/null && [ -f ~/.local/share/nvim/site/autoload/plug.vim ]; then
  1571.             echo
  1572.             echo -e "${BRIGHT_BLUE}::${RESET} Updating user nvim plugins . . ."
  1573.             nvim --headless +'silent! PlugUpgrade' +'silent! PlugUpdate' +qall
  1574.             echo
  1575.  
  1576.             if sudo -n true 2>/dev/null; then
  1577.                 echo -e "${BRIGHT_BLUE}::${RESET} Updating root nvim plugins . . ."
  1578.                 sudo -EH nvim --headless +'silent! PlugUpgrade' +'silent! PlugUpdate' +qall
  1579.             else
  1580.                 echo -e "${BRIGHT_BLUE}::${RESET} Skipping root nvim plugins (no sudo)."
  1581.             fi
  1582.         fi
  1583.     }
  1584.     update_vim_plugins
Advertisement
Add Comment
Please, Sign In to add comment