From d67a9f0f93b48d7b11d0bc24b0bcb7fca0d070c0 Mon Sep 17 00:00:00 2001 From: Faye Lorenz Date: Fri, 27 Feb 2026 00:02:50 -0600 Subject: [PATCH] init --- dot_bashrc | 11842 ++++++++++++++++++++++++++++ dot_bashrc_help | 987 +++ dot_config/fastfetch/config.jsonc | 84 + dot_config/ghostty/config | 4 + dot_config/hyfetch.json | 15 + dot_envrc | 1 + dot_gvimrc | 85 + dot_vim/after/syntax/c.vim | 4 + dot_vim/after/syntax/css.vim | 3 + dot_vim/after/syntax/csv.vim | 29 + dot_vim/after/syntax/html.vim | 3 + dot_vim/after/syntax/markdown.vim | 33 + dot_vim/after/syntax/vim.vim | 5 + dot_vim/autoload/plug.vim | 2901 +++++++ dot_vim/coc-settings.json | 38 + dot_vimrc | 1882 +++++ 16 files changed, 17916 insertions(+) create mode 100644 dot_bashrc create mode 100644 dot_bashrc_help create mode 100644 dot_config/fastfetch/config.jsonc create mode 100644 dot_config/ghostty/config create mode 100644 dot_config/hyfetch.json create mode 100644 dot_envrc create mode 100644 dot_gvimrc create mode 100644 dot_vim/after/syntax/c.vim create mode 100644 dot_vim/after/syntax/css.vim create mode 100644 dot_vim/after/syntax/csv.vim create mode 100644 dot_vim/after/syntax/html.vim create mode 100644 dot_vim/after/syntax/markdown.vim create mode 100644 dot_vim/after/syntax/vim.vim create mode 100644 dot_vim/autoload/plug.vim create mode 100644 dot_vim/coc-settings.json create mode 100644 dot_vimrc diff --git a/dot_bashrc b/dot_bashrc new file mode 100644 index 0000000..f893a79 --- /dev/null +++ b/dot_bashrc @@ -0,0 +1,11842 @@ +#!/usr/bin/env bash + +eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv bash)" + +####################################################### +# ┏━╸╻ ╻╺┳╸┏━┓┏━╸┏┳┓┏━╸ ╻ ╻╻ ╺┳╸╻┏┳┓┏━┓╺┳╸┏━╸ ┏┓ ┏━┓┏━┓╻ ╻┏━┓┏━╸ +# ┣╸ ┏╋┛ ┃ ┣┳┛┣╸ ┃┃┃┣╸ ┃ ┃┃ ┃ ┃┃┃┃┣━┫ ┃ ┣╸ ┣┻┓┣━┫┗━┓┣━┫┣┳┛┃ +# ┗━╸╹ ╹ ╹ ╹┗╸┗━╸╹ ╹┗━╸ ┗━┛┗━╸ ╹ ╹╹ ╹╹ ╹ ╹ ┗━╸ ╹┗━┛╹ ╹┗━┛╹ ╹╹┗╸┗━╸ +# https://sourceforge.net/projects/ultimate-bashrc/files/ +# Extreme Ultimate .bashrc File sources are free and +# open software released under the Zero-Clause BSD License (0BSD) +# https://opensource.org/license/0BSD +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted. +# +# THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +# AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +# OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +# PERFORMANCE OF THIS SOFTWARE. +####################################################### +# To Install or Update: +# wget -O ~/.bashrc https://sourceforge.net/projects/ultimate-bashrc/files/_bashrc/download +# wget -O ~/.bashrc_help https://sourceforge.net/projects/ultimate-bashrc/files/_bashrc_help/download +# -or- +# curl -L --output ~/.bashrc https://sourceforge.net/projects/ultimate-bashrc/files/_bashrc/download +# curl -L --output ~/.bashrc_help https://sourceforge.net/projects/ultimate-bashrc/files/_bashrc_help/download +####################################################### +# Supported Optional Applications/Dependencies: +# 7z apg aria2 atuin aureport baca base64 bash-completion bashmarks bashtop bat +# batcat bc blesh bottom bpytop broot btm btop btrfs bzip2 ccat cmatrix cod +# colordiff commacd cracklib curl delta diff-so-fancy difftastic dircolors +# distrobox doas doasedit dua dust dym elinks enhancd exa exiftool eza fasd fd +# ffmpeg figlet fresh frogmouth fx fzf fzf-tab-completion fzy gawk gcal gdu gio +# git git-commander git-completion git-delta gitalias gitui glances glow gpg grc +# grv gtop gunzip gzip hBlock helix hstr htop icdiff ifconfig iftop imagemagick +# iotop jless jnv jp2a jq kdiff3 keepass keepassxc keeweb lazygit links links2 +# lnav lolcat lscolors lsd lsof lsx lynx mc mcfly mdcat mdless meld micro +# mlocate moar most multitail mysql-colorize nano ncdu neovim nethogs nmon nnn +# nvtop openssl paru pkill pwgen qfc ranger rar rem resh restore-trash rhvoice +# ripgrep rsync shellcheck shred silver_searcher skim source-highlight sshpass +# tar termdown terminology thefuck tig tmux toilet trash-cli tree tuifi ugit +# vivid vizex vlock w3m wget wl-copy wl-paste xclip xdg-open xdotool xhost +# xprop xrdb xsel youtube-dl yt-dlp ytfzf ytop zellij zf zip zoxide +# +# Supported Optional Huds: +# neofetch, fastfetch, screenFetch, linux_logo, archey, pfetch +# +# Supported Optional Prompts: +# Trueline, Powerline, Powerline-Go, Powerline-Shell, +# Pureline, Starship, Bash Git Prompt, Liquid Prompt +# +# To Install Packages: +# pkginstall [package names separated by spaces] +####################################################### +# set -o errexit # Exit when a command fails +# set -o pipefail # Catch mysqldump fails +# set -o nounset # Exit when using undeclared variables +# set -o xtrace # Trace what gets executed (useful for debugging) + +### ERROR TRAPPING +# alias debug="set -o nounset; set -o xtrace" +# error() { echo 'Error in ${1} on line ${2}: ${3}' } +# trap 'error "${BASH_SOURCE}" "${LINENO}"' ERR + +### TEST FOR AN INTERACTIVE SHELL +# This file is sourced by all "interactive" bash shells on startup +# including shells such as scp and rcp that can't tolerate any output. +# There is no need to set anything past this point for scp and rcp, +# and it's important to refrain from outputting anything in those cases. +[[ $- != *i* ]] && return +[[ -z "$PS1" ]] && return + +# Bash version check +if [ -n "$BASH_VERSION" ] && ((BASH_VERSINFO[0] < 4)); then + echo "This .bashrc file requires at least Bash 4.0" + return 1 +fi + +# Source global definitions from the available bashrc files +if [[ -f /etc/bashrc ]]; then + builtin source /etc/bashrc +elif [[ -f /etc/bash.bashrc ]]; then + builtin source /etc/bash.bashrc +fi + +# Grant permission to the local root user to access the X server +# This is NOT recommended for regular use due to the security implications: +# https://stackoverflow.com/questions/63884968/why-is-xhost-considered-dangerous +#if [[ -x "$(command -v xhost)" ]]; then +# # 'xhost +local:root' allows the root user to connect to the X server +# # This might be needed for some X applications to work +# xhost +local:root > /dev/null 2>&1 +#fi + +####################################################### +# Default Bash Escape ANSI Color Codes +####################################################### + +# Foreground Colors +BLACK="\033[0;30m" +RED="\033[0;31m" +GREEN="\033[0;32m" +YELLOW="\033[0;33m" +BLUE="\033[0;34m" +MAGENTA="\033[0;35m" +CYAN="\033[0;36m" +WHITE="\033[0;37m" + +# Bright Foreground Colors +BRIGHT_BLACK="\033[1;30m" +BRIGHT_RED="\033[1;31m" +BRIGHT_GREEN="\033[1;32m" +BRIGHT_YELLOW="\033[1;33m" +BRIGHT_BLUE="\033[1;34m" +BRIGHT_MAGENTA="\033[1;35m" +BRIGHT_CYAN="\033[1;36m" +BRIGHT_WHITE="\033[1;37m" + +# Background Colors +BG_BLACK="\033[0;40m" +BG_RED="\033[0;41m" +BG_GREEN="\033[0;42m" +BG_YELLOW="\033[0;43m" +BG_BLUE="\033[0;44m" +BG_MAGENTA="\033[0;45m" +BG_CYAN="\033[0;46m" +BG_WHITE="\033[0;47m" + +# Bright Background Colors +BG_BRIGHT_BLACK="\033[1;40m" +BG_BRIGHT_RED="\033[1;41m" +BG_BRIGHT_GREEN="\033[1;42m" +BG_BRIGHT_YELLOW="\033[1;43m" +BG_BRIGHT_BLUE="\033[1;44m" +BG_BRIGHT_MAGENTA="\033[1;45m" +BG_BRIGHT_CYAN="\033[1;46m" +BG_BRIGHT_WHITE="\033[1;47m" + +# Reset Color +RESET="\033[0m" + +####################################################### +# Find a temp directory where local is preferred +####################################################### + +# Loop through potential temporary directories in order of preference +for _TEMP_DIR_PATH in \ + "${XDG_CACHE_HOME:-${HOME}/.cache}/tmp" \ + "${HOME}/.tmp" \ + "${HOME}/.temp" \ + "${XDG_CACHE_HOME:-${HOME}/.cache}" \ + "${TMPDIR}" \ + "${TMP}" \ + "/tmp" \ + "/temp"; do + + # Check if directory exists + if [[ -d "${_TEMP_DIR_PATH}" ]]; then + TEMPDIR_LOCAL="${_TEMP_DIR_PATH}" + break # Exit loop after finding first valid directory + fi +done + +# Clean up +unset _TEMP_DIR_PATH + +####################################################### +# Set the default editor +# Examples: vim, nvim, emacs, nano, micro, fresh, helix, pico, +# or gui apps like kate, gedit, notepadqq, or vscodium +# NOTE: In Git Bash, you can use something like "/c/Program\ Files/Notepad++/notepad++.exe" +# To change these without modifying this file, simply +# add these with your changes to one of these files: +# ~/.env +# ~/.envrc +# ~/.config/bashrc/config +# See section "Configuration and Extras" in the README: +# Link: https://sourceforge.net/projects/ultimate-bashrc/files/ +####################################################### + +# Declare associative array for caching command checks +declare -A _HASCOMMAND_CACHE + +# Check if a command or alias exists (with caching for performance) +function hascommand() { + # If no arguments, just '--strict', or help requested, show help message + if [[ -z "${1}" || "${1}" == "--help" || "${1}" == "-h" || (${#} -eq 1 && "${1}" == "--strict") ]]; then + echo -e "${BRIGHT_CYAN}hascommand${RESET}: Check if a command or alias exists" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}hascommand${RESET} ${BRIGHT_MAGENTA}[${BRIGHT_GREEN}options${BRIGHT_MAGENTA}]${RESET} ${BRIGHT_MAGENTA}<${BRIGHT_YELLOW}command${BRIGHT_MAGENTA}>${RESET}" + echo -e "${BRIGHT_WHITE}Options:${RESET}" + echo -e " ${BRIGHT_GREEN}--strict${RESET}, ${BRIGHT_GREEN}-s${RESET} Check executables only (exclude aliases)" + echo -e " ${BRIGHT_GREEN}--no-cache${RESET} Skip cache (useful after installing new software)" + echo -e "${BRIGHT_WHITE}Examples:${RESET}" + echo -e " ${BRIGHT_CYAN}hascommand${RESET} ${BRIGHT_YELLOW}ls${RESET} ${BRIGHT_BLUE}# Check any command or alias${RESET}" + echo -e " ${BRIGHT_CYAN}hascommand${RESET} ${BRIGHT_GREEN}--strict${RESET} ${BRIGHT_YELLOW}grep${RESET} ${BRIGHT_BLUE}# Check executable only${RESET}" + echo -e " ${BRIGHT_CYAN}hascommand${RESET} ${BRIGHT_GREEN}--no-cache --strict${RESET} ${BRIGHT_YELLOW}newcmd${RESET} ${BRIGHT_BLUE}# Force fresh check${RESET}" + return 2 # Return code 2 to indicate incorrect usage + fi + + # Check for --no-cache flag + local USE_CACHE=true + if [[ "${1}" == "--no-cache" ]]; then + USE_CACHE=false + shift + fi + + # Check for the '--strict' option + local STRICT=false + local COMMAND + if [[ "${1}" == "--strict" ]] || [[ "${1}" == "-s" ]]; then + STRICT=true + COMMAND="${2}" + else + COMMAND="${1}" + fi + + # Create cache key + local CACHE_KEY="${STRICT}:${COMMAND}" + + # Check cache first + if [[ "${USE_CACHE}" == true ]] && [[ -n "${_HASCOMMAND_CACHE[${CACHE_KEY}]}" ]]; then + [[ "${_HASCOMMAND_CACHE[${CACHE_KEY}]}" == "1" ]] + return $? + fi + + # Perform the actual check + local RESULT + if [[ "${STRICT}" == true ]]; then + # Look for executable command using type -P + if type -P "${COMMAND}" &>/dev/null; then + RESULT=1 + else + RESULT=0 + fi + else + # Look for command or alias + if type "${COMMAND}" &>/dev/null; then + RESULT=1 + else + RESULT=0 + fi + fi + + # Cache the result + _HASCOMMAND_CACHE[${CACHE_KEY}]="${RESULT}" + + [[ "${RESULT}" == "1" ]] + return $? +} + +# Helper to clear the cache if you install something mid-session +function hascommandclear() { + # Help text + if [[ "${1}" == "--help" ]] || [[ "${1}" == "-h" ]]; then + echo -e "${BRIGHT_CYAN}hascommandclear${RESET}: Clear the hascommand lookup cache" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}hascommandclear${RESET}" + echo -e "${BRIGHT_WHITE}Description:${RESET}" + echo -e " ${BRIGHT_BLUE}Run after installing new software to refresh command detection${RESET}" + return 0 + fi + + unset _HASCOMMAND_CACHE + declare -gA _HASCOMMAND_CACHE + echo "hascommand cache cleared" +} + +# BEGIN_EDITOR_CONFIG +# Loop a list of common editors to check +for _EDITOR_NAME in \ + fresh micro ne helix tilde jed vile zile joe nano nvim vim emacs vi jove mg ed +do + # Order matters: plain name first, then Flatpak and Snap host paths + for EDITOR in \ + "${_EDITOR_NAME}" \ + "/usr/local/bin/${_EDITOR_NAME}" \ + "${HOME}/.local/bin/${_EDITOR_NAME}" \ + "/run/host/usr/bin/${_EDITOR_NAME}" \ + "/var/run/host/usr/bin/${_EDITOR_NAME}" \ + "/snap/bin/${_EDITOR_NAME}" + do + # Accept commands, aliases, or executable absolute paths + if command -v "${EDITOR}" >/dev/null 2>&1 || [ -x "${EDITOR}" ]; then + + # Default text editor for various command-line utilities + # (fallback if VISUAL is not set) + export EDITOR + + # Default text editor for visual (full-screen) utilities + # (takes precedence over EDITOR) + export VISUAL="${EDITOR}" + + # Specifies the editor to use with 'sudo -e' or 'sudoedit' + # (overrides VISUAL and EDITOR) + export SUDO_EDITOR="${EDITOR}" + + # Specifies the editor for 'fc' command to edit and re-run + # commands from history (falls back to EDITOR) + export FCEDIT="${EDITOR}" + + # Specifies a fallback editor for Emacs and its derivatives + # (Used when Emacs cannot start the primary editor defined by EDITOR) + export ALTERNATE_EDITOR="${EDITOR}" + + # nnn default action for opening a file + # https://github.com/jarun/nnn + export NNN_OPENER="${EDITOR}" + + break 2 # Found so exit both loops + fi + done +done + +# Clean up +unset _EDITOR_NAME +# END_EDITOR_CONFIG + +# We will default to use either Neovim https://neovim.io or vim instead of vi +# NOTE: vi is POSIX compliant but vim has more features and Neovim is more extensible +# http://www.viemu.com/a-why-vi-vim.html +# https://www.linuxfordevices.com/tutorials/linux/vim-vs-neovim +if hascommand --strict nvim; then + alias {v,vi,vim}='nvim' + alias svi='sudo nvim' + alias vis='nvim "+set si"' +elif hascommand --strict vim; then + alias {v,vi}='vim' + alias svi='sudo vim' + alias vis='vim "+set si"' +elif hascommand --strict vi; then + alias v='vi' + alias svi='sudo vi' +fi + +# Set some defaults for nano +# NOTE: Depending on the version of nano you have, --linenumbers and --suspend is helpful +if hascommand --strict nano; then + alias {n,nano}='nano --smarthome --multibuffer --const --autoindent' +fi + +# Set Micro editor true color support +# Link: https://micro-editor.github.io/ +# Install: curl https://getmic.ro | bash +export MICRO_TRUECOLOR=1 + +# Create an alias for the Helix editor +# See hx --tutor or :tutor for a vimtutor-like introduction +# Link: https://github.com/helix-editor/helix +# Link: https://docs.helix-editor.com/title-page.html +if hascommand --strict helix; then + alias hx='helix' +fi + +# Smart File Editor with Auto Privilege Management +# Uses the default editor or sudoedit for security based on file permissions +# It also provides visual feedback in color and even integrates with Tmux +# Syntax: edit [optional_filename] +alias e="edit" +function edit() { + + # Local variable to track if the immutable attribute was modified + local IMMUTABLE_SET="" + # Track the current tmux tab name so restore_tab can reset it + local _CURRENT_TAB_NAME="" + + # NOTE: The helper functions are defined inside this edit function to avoid + # cluttering the global namespace and tab completion. They are specific to + # the edit functionality and not intended to be used independently. + + ### Post-edit action function to handle specific file edits + # and reapply the immutable attribute if it was removed + function post_edit_action() { + # Convert the provided path to an absolute path + local ABSOLUTE_PATH=$(realpath "${1}" 2>/dev/null) + + # Exit the function if the file does not exist + [[ -z "${ABSOLUTE_PATH}" ]] && return + + # Use a case statement to match the filename with specific actions + case "${ABSOLUTE_PATH}" in + + /etc/default/grub) + # Check if grub-mkconfig exists before updating grub configuration + if hascommand grub-mkconfig; then + if ask "${BRIGHT_YELLOW}Update grub configuration?${RESET}" Y; then + sudo grub-mkconfig -o /boot/grub/grub.cfg + echo -e "${BRIGHT_GREEN}Grub configuration updated${RESET}" + fi + fi + ;; + + # Apache Configuration + /etc/httpd/conf/httpd.conf|/etc/apache2/apache2.conf) + # Validate Apache Configuration + if hascommand apachectl; then + if apachectl configtest; then + if ask "${BRIGHT_YELLOW}Restart Apache to apply changes?${RESET}" Y; then + apacherestart + fi + else + echo -e "${BRIGHT_RED}Apache configuration test failed${RESET}" + fi + fi + ;; + + # Nginx Configuration + /etc/nginx/nginx.conf) + # Validate Nginx Configuration + if hascommand --strict nginx; then + if sudo nginx -t; then + if ask "${BRIGHT_YELLOW}Restart Nginx to apply changes?${RESET}" Y; then + ngrestart + fi + else + echo -e "${BRIGHT_RED}Nginx configuration test failed${RESET}" + fi + fi + ;; + + *php.ini|*/php/*/php.ini) + if hascommand --strict php; then + if ask "${BRIGHT_YELLOW}Restart web servers to apply PHP changes?${RESET}" Y; then + # Try to restart Apache and suppress errors + if hascommand --strict apachectl || hascommand --strict httpd || hascommand --strict apache2; then + apacherestart + fi + + # Try to restart Nginx and suppress errors + if hascommand --strict nginx; then + ngrestart + fi + + echo -e "${BRIGHT_GREEN}Attempted to restart web servers for PHP.ini changes${RESET}" + fi + fi + ;; + + /etc/ssh/sshd_config) + # Check if systemd is installed and systemctl is available + if hascommand --strict systemctl; then + if ask "${BRIGHT_YELLOW}Restart SSH service to apply changes?${RESET}" Y; then + # Directly execute the commands to restart and enable the SSH service + sudo systemctl restart sshd.service && sudo systemctl enable sshd.service + echo -e "${BRIGHT_GREEN}SSH service restarted${RESET}" + fi + fi + ;; + + /etc/vconsole.conf) + # Check if mkinitcpio exists before rebuilding the initial ramdisk environment + if hascommand --strict mkinitcpio; then + if ask "${BRIGHT_YELLOW}Rebuild initial ramdisk environment for vconsole?${RESET}" Y; then + sudo mkinitcpio -P + echo -e "${BRIGHT_GREEN}Initial ramdisk environment rebuilt${RESET}" + fi + fi + ;; + + "${HOME}/.bash_profile") + # If editing the .bash_profile file, reload it + if ask "${BRIGHT_YELLOW}Reload the new .bash_profile file?${RESET}" N; then + command clear + builtin source ~/.bash_profile + fi + ;; + + "${HOME}/.bashrc") + # If editing the .bashrc file, reload it + if ask "${BRIGHT_YELLOW}Reload the new .bashrc file?${RESET}" N; then + command clear + builtin source ~/.bashrc + fi + ;; + + "${HOME}/.tmux.conf"|"${XDG_CONFIG_HOME:-${HOME}/.config}/tmux/tmux.conf"|/etc/tmux.conf) + # If editing .tmux.conf, refresh tmux configuration + # Check if tmux is installed and running + if hascommand --strict tmux && tmux info &> /dev/null; then + tmux source-file "${1}" + echo -e "${BRIGHT_GREEN}Reloaded Tmux configuration from ${BRIGHT_MAGENTA}${1}${RESET}" + fi + ;; + + "${HOME}/.Xresources"|/etc/X11/Xresources) + # Check if xrdb exists and we are in X11 before merging .Xresources + if [[ $DISPLAY ]] && [[ $XDG_SESSION_TYPE == "x11" ]] && hascommand --strict xrdb; then + if ask "${BRIGHT_YELLOW}Merge X resources from ${ABSOLUTE_PATH}?${RESET}" Y; then + xrdb -merge "${1}" + echo -e "${BRIGHT_GREEN}Merged X resources from ${BRIGHT_MAGENTA}${1}${RESET}" + fi + fi + ;; + + # -- Post-edit file format validators (read-only, no restart needed) -- + + *.desktop) + # Validate .desktop file after editing + if hascommand desktop-file-validate; then + if desktop-file-validate "${1}"; then + echo -e "${BRIGHT_GREEN}Desktop file validation passed${RESET}" + else + echo -e "${BRIGHT_RED}Desktop file validation failed${RESET}" + fi + fi + ;; + + *.json) + # Validate JSON syntax after editing + if hascommand jq; then + if jq 'empty' "${1}" 2>/dev/null; then + echo -e "${BRIGHT_GREEN}JSON validation passed${RESET}" + else + echo -e "${BRIGHT_RED}JSON validation failed${RESET}" + # Show the actual error for context + jq 'empty' "${1}" 2>&1 + fi + fi + ;; + + *.xml) + # Validate XML syntax after editing + if hascommand xmllint; then + if xmllint --noout "${1}" 2>/dev/null; then + echo -e "${BRIGHT_GREEN}XML validation passed${RESET}" + else + echo -e "${BRIGHT_RED}XML validation failed${RESET}" + # Show the actual error for context + xmllint --noout "${1}" 2>&1 + fi + fi + ;; + + *.yaml|*.yml) + # Validate YAML syntax after editing + if hascommand yamllint; then + if yamllint -s "${1}" 2>/dev/null; then + echo -e "${BRIGHT_GREEN}YAML validation passed${RESET}" + else + echo -e "${BRIGHT_RED}YAML validation failed${RESET}" + # Show the actual errors for context + yamllint -s "${1}" 2>&1 + fi + fi + ;; + + *.service|*.timer|*.socket|*.mount|*.path|*.target|*.slice) + # Validate systemd unit file after editing + if hascommand systemd-analyze; then + if systemd-analyze verify "${1}" 2>/dev/null; then + echo -e "${BRIGHT_GREEN}Systemd unit validation passed${RESET}" + else + echo -e "${BRIGHT_RED}Systemd unit validation failed${RESET}" + # Show the actual errors for context + systemd-analyze verify "${1}" 2>&1 + fi + fi + ;; + esac + } + + ### Helper function to use the more secure sudoedit only if it's possible + # since sudoedit cannot edit a file in a directory with write permissions + function sudo_edit() { + + # Determine the directory containing the file (SC2155: split declaration) + local DIRNAME + DIRNAME=$(dirname "${1}") + [[ "${DIRNAME}" == "." ]] && DIRNAME="${PWD}" + + # If inside a Tmux session, rename the tab temporarily + rename_tab "${1}" + + # Pre-authenticate so the status message appears after any password prompt + if ! sudo -v; then + restore_tab + return 1 + fi + + # Show what we're about to edit or create + if [[ -f "${1}" ]]; then + echo -e "${BRIGHT_GREEN}Editing ${BRIGHT_CYAN}$(tilde_path "${1}")${RESET}" + else + echo -e "${BRIGHT_GREEN}Creating ${BRIGHT_CYAN}$(tilde_path "${1}")${RESET}" + fi + + # Check if the directory is writable... + if [[ -w "${DIRNAME}" ]] || ! hascommand sudoedit; then + # Edit as root keeping environment variables (like default editor) + sudo --preserve-env "${EDITOR}" "${1}" && post_edit_action "${1}" + else + # Securely edit as root via temporary copy minimizing editor risks + sudoedit "${1}" && post_edit_action "${1}" + fi + + # Restore the Tmux tab name + restore_tab + } + + ### Check and modify the immutable attribute of a file + # Parameters: + # $1 - The filename to check and potentially modify the immutable attribute + function immutable_remove() { + # Check if required commands exist + if ! hascommand chattr || ! hascommand lsattr; then + return + fi + + # Check if the file and commands exists + if [[ -f "${1}" ]] && hascommand chattr && hascommand lsattr; then + + # Extract file attributes using lsattr + local ATTRIBUTES=$(lsattr "${1}" 2>/dev/null | cut -d' ' -f1) + + # Check if the immutable attribute is set (indicated by 'i') + if [[ "${ATTRIBUTES}" =~ i ]]; then + + # Ask user if they want to temporarily remove the immutable attribute to allow editing + if ask "${BRIGHT_YELLOW}This file is immutable. Temporarily remove immutable attribute to edit?${RESET}" Y; then + + # Try to remove the immutable attribute without using sudo first + if ! chattr -i "${1}" > /dev/null 2>&1; then + # If the command fails, inform the user and retry with sudo + sudo chattr -i "${1}" > /dev/null 2>&1 + fi + + # Set a flag to indicate the attribute was modified + IMMUTABLE_SET="true" + else + + # If the user chooses not to edit the file, exit and show a message + echo -e "${BRIGHT_RED}Cannot edit the file without removing the immutable attribute.${RESET}" + + # Exit the function with status 1 indicating an error + return 1 + fi + fi + fi + } + + ### Reapply the immutable attribute if it was previously removed + function immutable_restore() { + # Check if the file and commands exists + if [[ -f "${1}" ]] && hascommand chattr; then + + # If we changed the immutable attribute + if [[ "${IMMUTABLE_SET}" == "true" ]]; then + + # Try to restore the immutable attribute without using sudo first + if ! chattr +i "${1}" > /dev/null 2>&1; then + # If the command fails, inform the user and retry with sudo + sudo chattr +i "${1}" > /dev/null 2>&1 + fi + + echo -e "${BRIGHT_GREEN}Immutable attribute reapplied to ${BRIGHT_MAGENTA}${1}${RESET}" + fi + fi + } + + ### Helper function to rename the current Tmux or console tab if supported + function rename_tab() { + # Get the simple editor name + local _EDITOR_SHORT_NAME=$(basename "${EDITOR%% *}") + + # Get the filename (drop the path) for the new tab name + local _EDITING_FILENAME=$(basename "${1}") + + # If in a Tmux session... + if [[ -n "${TMUX}" ]] && hascommand tmux; then + # Save the current tab text + _CURRENT_TAB_NAME=$(tmux display-message -p '#W') + + # Rename the current tab + #tmux rename-window "✒️${_EDITING_FILENAME}" + tmux rename-window "${_EDITOR_SHORT_NAME}→${_EDITING_FILENAME}" + + # Check if we are in WezTerm + elif [[ "${TERM_PROGRAM}" == "WezTerm" ]]; then + # Rename the WezTerm tab + wezterm cli set-tab-title "${_EDITOR_SHORT_NAME} → ${_EDITING_FILENAME}" + + # We are not in Tmux... + else + # Change the tab title if the terminal supports it + echo -ne "\033]2;${_EDITOR_SHORT_NAME}→${_EDITING_FILENAME}\007" + echo -ne "\033]30;${_EDITOR_SHORT_NAME}→${_EDITING_FILENAME}\007" + fi + } + + ### Helper function to restore the Tmux or Konsole tab name + function restore_tab() { + # If we are inside a Tmux session... + if [[ -n "${TMUX}" ]] && [[ -n "${_CURRENT_TAB_NAME}" ]] && hascommand tmux; then + # Restore Tmux tab name + tmux rename-window "${_CURRENT_TAB_NAME}" + unset _CURRENT_TAB_NAME + + elif [[ "${TERM_PROGRAM}" == "WezTerm" ]]; then + # Restore WezTerm tab name + wezterm cli set-tab-title "" + + # If we are in Konsole... + elif [[ -n "$KONSOLE_DBUS_SERVICE" ]]; then + # Reset the tab title to the default + echo -ne "\033]30;%d : %n\007" + + # We are not in Tmux... + else + # This will reset the tab + echo -ne "\033]0;\007" + fi + } + + ### Replace home directory with ~ in displayed paths for privacy + function tilde_path() { + if [[ "${1}" == "${HOME}"* ]]; then + echo "~${1#"${HOME}"}" + else + echo "${1}" + fi + } + + ### Track whether the file path was already announced (e.g. by "Found") + local PATH_ANNOUNCED="" + + ### Check if the file is a symlink + local ACTUAL_FILE + if [[ -L "${1}" ]]; then + # Resolve the symlink to the actual file (SC2155: split declaration) + ACTUAL_FILE=$(resolvesymlink "${1}") + + # Inform the user about the switch + echo -e "${BRIGHT_YELLOW}Editing actual file instead of symlink:${RESET} ${BRIGHT_CYAN}$(tilde_path "${ACTUAL_FILE}")${RESET}" + else + # If not a symlink, proceed with the original file + ACTUAL_FILE="${1}" + fi + + ### If it does not contain a path and is not a file + if [[ "$ACTUAL_FILE" != */* && ! -f "$ACTUAL_FILE" ]]; then + # Try to locate the full path using `which` + local FOUND_PATH=$(command which "$ACTUAL_FILE" 2>/dev/null) + + # Update ACTUAL_FILE if a path was found + if [[ -n "$FOUND_PATH" ]]; then + ACTUAL_FILE="$FOUND_PATH" + echo -e "${BRIGHT_GREEN}Found ${BRIGHT_CYAN}$(tilde_path "${ACTUAL_FILE}")${RESET}" + PATH_ANNOUNCED="true" + fi + fi + + ### Check for special case of editing /etc/sudoers... + if [[ "${ACTUAL_FILE}" == "/etc/sudoers" ]] && hascommand visudo; then + echo -ne "${BRIGHT_RED}Security alert:" + echo -e "${BRIGHT_YELLOW} Using visudo to edit ${BRIGHT_CYAN}${ACTUAL_FILE}${RESET}" + if ! immutable_remove "${1}"; then + # For some reason, we are unable to remove the immutable attribute + return 1 + fi + rename_tab "${ACTUAL_FILE}" + sudo visudo + restore_tab + immutable_restore "${1}" + return + + ### Check if EDITOR is set... + elif [ -z "${EDITOR}" ]; then + echo -ne "${BRIGHT_RED}Error: ${BRIGHT_CYAN}EDITOR environment variable is not set.${RESET}" + return 1 + + ### No parameters passed, load the default editor... + elif [[ $# -eq 0 ]]; then + rename_tab 'New File' + "${EDITOR}" + restore_tab + return + + ### Check if file exists and has read/write permissions... + elif [[ -r "${ACTUAL_FILE}" ]] && [[ -w "${ACTUAL_FILE}" ]]; then + immutable_remove "${1}" + rename_tab "${ACTUAL_FILE}" + + # Get the initial modification time of the file + # Use platform-appropriate stat flag (GNU vs BSD/macOS) + local _STAT_FMT='-c %Y' + [[ "$(uname)" == "Darwin" ]] && _STAT_FMT='-f %m' + local INITIAL_MTIME="" + if [[ -f "${ACTUAL_FILE}" ]]; then + INITIAL_MTIME=$(stat ${_STAT_FMT} "${ACTUAL_FILE}") + fi + + # Show the file being edited (skip if already announced by "Found") + if [[ -z "${PATH_ANNOUNCED}" ]]; then + echo -e "${BRIGHT_GREEN}Editing ${BRIGHT_CYAN}$(tilde_path "${ACTUAL_FILE}")${RESET}" + fi + + # Capture the exit status of the editor + "${EDITOR}" "${ACTUAL_FILE}" + local EDIT_STATUS=$? + + # Check if the file was modified based on the modification time + if [[ -f "${ACTUAL_FILE}" ]]; then + if [[ "$(stat ${_STAT_FMT} "${ACTUAL_FILE}")" != "${INITIAL_MTIME}" ]]; then + post_edit_action "${ACTUAL_FILE}" + fi + else + post_edit_action "${ACTUAL_FILE}" + fi + + restore_tab + immutable_restore "${1}" + return $EDIT_STATUS + + ### Check if the file exists but doesn't have write permission... + elif [[ -f "${ACTUAL_FILE}" ]]; then + echo -ne "${BRIGHT_RED}Insufficient permissions:" + echo -e "${BRIGHT_YELLOW} Using super user to edit ${BRIGHT_CYAN}${ACTUAL_FILE}${RESET}" + immutable_remove "${1}" + sudo_edit "${ACTUAL_FILE}" + immutable_restore "${1}" + return + + ### File doesn't exist, check if we can create it... + elif [[ -w "$(dirname "${ACTUAL_FILE}")" ]]; then + echo -e "${BRIGHT_GREEN}Creating ${BRIGHT_CYAN}$(tilde_path "${ACTUAL_FILE}")${RESET}" + # Attempt to edit the file + rename_tab "${ACTUAL_FILE}" + if "${EDITOR}" "${ACTUAL_FILE}"; then + return + else # There was an error... + # Retry with sudo_edit + restore_tab + echo -ne "${BRIGHT_RED}Insufficient permissions:" + echo -e "${BRIGHT_YELLOW} Retrying with super user to edit ${BRIGHT_CYAN}${ACTUAL_FILE}${RESET}" + immutable_remove "${1}" + sudo_edit "${ACTUAL_FILE}" + immutable_restore "${1}" + return + fi + + ### We need super user access to create the new file + else + echo -ne "${BRIGHT_RED}Insufficient permissions:" + echo -e "${BRIGHT_YELLOW} Using super user to create ${BRIGHT_CYAN}${ACTUAL_FILE}${RESET}" + sudo_edit "${ACTUAL_FILE}" + return + fi +} + +# Command-line completion for the edit command +_edit_completion() { + local cur prev words cword + _init_completion -n = || return + + case $prev in + edit|e) + _filedir + return + ;; + esac +} +complete -F _edit_completion edit e + +# Shortcut for edit using root permissions +alias se="sudoedit" + +####################################################### +# Set default variable values +# These variables can be overridden in one of these environment variable files: +# ~/.env +# ~/.envrc +# ~/.config/bashrc/config +####################################################### + +# BEGIN_BASHRC_CONFIG +# Determines if CTRL-h will show help +# Ctrl+h (for help) and Ctrl+Backspace share the same key binding +# in some terminal emulators so we default to skip this keybind +_SKIP_HELP_KEYBIND=true + +# Show an installed information HUD on initial Bash load (if not skipped) +# Link: https://github.com/LinusDierheimer/fastfetch +# Link: https://ostechnix.com/neofetch-display-linux-systems-information/ +# Link: https://github.com/KittyKatt/screenFetch +# Link: https://github.com/deater/linux_logo +# Link: https://github.com/dylanaraps/pfetch +_SKIP_SYSTEM_INFO=false + +# If not skipped, shows pending updates (only in Arch, Manjaro, and Ubuntu) +# WARNING: This check for updates takes several seconds so the default is true +_SKIP_UPGRADE_NOTIFY=true + +# Automatically launch TMUX terminal multiplexer in local, TTY, or SSH sessions +# https://github.com/tmux/tmux/wiki +# Since TMUX is pre-installed on so many systems, the default is to skip TMUX +_TMUX_LOAD_TTY=false +_TMUX_LOAD_SSH=false +_TMUX_LOAD_LOCAL=false + +# OPTIONAL: Set and force the default TMUX session name for this script and tm +# If not specified, an active TMUX session is used and attached to +# If no active TMUX session exists, the current logged in user name is used +#_TMUX_LOAD_SESSION_NAME="" + +# Terminology is a graphical EFL terminal emulator that can run in TTY sessions +# If installed, it can automatically be launched when starting a TTY session +# To split the window horizontally press Ctrl+Shift+PgUp +# To split the window vertically press Ctrl+Shift+PgDn +# To create Tabs press Ctrl+Shift+T and cycle through using Ctrl+1-9 +# Link: https://github.com/borisfaure/terminology +# Link: https://linoxide.com/terminology-terminal/ +_SKIP_TERMINOLOGY_TTY=false + +# Blesh: Bash Line Editor replaces default GNU Readline +# Link: https://github.com/akinomyoga/ble.sh +# Link for configuration: https://github.com/akinomyoga/ble.sh/blob/master/blerc +# WARNING: Can be buggy with certain prompts (like Trueline) +_SKIP_BLESH=false + +# Make sure the default file and directory permissions for newly created files +# in the home directory is umask 026 to improve security. +# (user=read/write/execute, group=read/execute, others=execute for directories) +# The default is to skip this security setting and not modify home permissions +_SKIP_UMASK_HOME=true + +# Replaces Sudo with one of the two alternatives (if installed): +# RootDO (rdo) - A very slim alternative to both sudo and doas +# Link: https://codeberg.org/sw1tchbl4d3/rdo +# - OR - +# A port of OpenBSD's doas offers two benefits over sudo: +# 1) Its configuration file has a simple syntax and +# 2) It is smaller, requiring less effort to audit the code +# Link: https://github.com/Duncaen/OpenDoas or https://github.com/slicer69/doas +# Default value is skip and must be set to false manually for security reasons +_SKIP_SUDO_ALTERNATIVE=true + +# If set to true, cd will not output the current absolute path under certain +# circumstances like when using the command cd - or using cdable_vars bookmarks +# Link: https://www.gnu.org/software/bash/manual/bash.html#index-cd +_SILENCE_CD_OUTPUT=false + +# If set to true, will not load anything that modifies the ls command or colors +_SKIP_LS_COLORIZED=false + +# LSD (LSDeluxe) is a rewrite of GNU ls with lots of added features like +# colors, icons, tree-view, more formatting options, git support, etc. +# Fonts: Install the patched fonts of powerline, nerd-font, and/or font-awesome +# Link: https://github.com/Peltoche/lsd +_SKIP_LSD=false + +# eza/exa is a modern color replacement for ls that also has some Git support +# Link: https://github.com/eza-community/eza +# Link: https://github.com/ogham/exa +_SKIP_EXA=false + +# grc Generic Colouriser +# Link: https://github.com/garabik/grc +_SKIP_GRC=false + +# Use built-in aliases for grc Generic Colouriser instead of it's own includes +_GRC_USE_BASHRC_BUILTIN=false + +# Choose your preferred picker to use with menus +# You can choose any picker like fzy, sk, fzf, peco, percol, pick, icepick, +# selecta, sentaku, zf, or even dmenu, rofi, or wofi UI pickers +_PREFERRED_PICKER= + +# If set to true, will not source bash completion scripts +_SKIP_BASH_COMPLETION=false + +# If set to true, will show a calendar when Bash is started +_SHOW_BASH_CALENDAR=false + +# If GNU gcal is installed, use this local for holidays +# To show the possible options type: gcal -hh | grep 'Holidays in' +# Link: https://www.gnu.org/software/gcal/manual/gcal.html +# Link: https://unix.stackexchange.com/questions/164555/how-to-emphasize-holidays-by-color-in-cal-command +_GCAL_COUNTRY_CODE=US_AK + +# Skip the birthday/anniversary reminder that shows a message in your teminal? +# Reads the birthday CSV file: ~/.config/birthdays.csv +# The first line is ignored (header) and the format is (year is optional): +# Month,Day,Year,"Message" +# Jan,1,1985,"This is a message!" +# +# Figlet and/or Toilet application is an optional dependency +# Install Arch/Manjaro: sudo pacman -S toilet +# Install Ubuntu/Debian: sudo apt-get install toilet +_SKIP_BDAY_REMINDER=false + +# Set the location for the birthday/anniversary reminder CSV file +# The default location is "~/.config/birthdays.csv" +_BDAY_FILE="${XDG_CONFIG_HOME:-${HOME}/.config}/birthdays.csv" + +# Set the preferred birthday reminder font here (default is "future"): +_BDAY_FONT=future + +# Set to have the built in prompt use a faster but less precise Git method +# This might be necessary on slow connections or networked directories +# Also if set to true, will remove eza/exa's --git flag (use lsg for Git info) +_GIT_IS_SLOW=false + +# Optional original prompt from 2014 version now with newly added Git support +# download the optional .bashrc_prompt script file and place it in either your +# home directory or as the file ~/.config/bashrc/prompt +# You will also need to make sure this setting is set to false +_SKIP_PROMPT_ORIGINAL=false + +# If false, the built-in prompt will be one single line with an abbreviated path +# If true, the built-in prompt will split into two lines with a full path +_PROMPT_BUILTIN_FULL_PATH=false + +# Trueline Bash (true 24-bit color and glyph support) +# This is the prefered prompt since it looks amazing, +# has so many features, is easily extended using functions, +# and is a single Bash script file that is easy to install. +# Link: https://github.com/petobens/trueline +# Install: wget https://raw.githubusercontent.com/petobens/trueline/master/trueline.sh -P ~/ +# Fonts: https://github.com/powerline/fonts +_SKIP_PROMPT_TRUELINE=false + +# Powerline-Go (this prompt uses no special glyphs) +# Link: https://github.com/justjanne/powerline-go +_SKIP_PROMPT_POWERLINE_GO=false + +# Powerline-Shell (details about git/svn/hg/fossil branch and Python virtualenv environment) +# Link: https://github.com/b-ryan/powerline-shell +_SKIP_PROMPT_POWERLINE_SHELL=false + +# Pureline (256 color written in bash script) +# Link: https://github.com/chris-marsh/pureline +# Install: +# git clone https://github.com/chris-marsh/pureline.git +# cp pureline/configs/powerline_full_256col.conf ~/.pureline.conf +_SKIP_PROMPT_PURELINE=false + +# Starship Cross Shell Prompt (focus on compatibility and written in Rust) +# Link: https://starship.rs +# Install: sh -c "$(curl -fsSL https://starship.rs/install.sh)" +_SKIP_PROMPT_STARSHIP=false + +# Oh-My-Git (only used for Git but has huge support for it, requires font) +# Link: https://github.com/arialdomartini/oh-my-git +# Install: git clone https://github.com/arialdomartini/oh-my-git.git ~/.oh-my-git +_SKIP_PROMPT_OH_MY_GIT=false + +# Bash Git Prompt (shows git repository, branch name, difference with remote branch, number of files staged, changed, etc) +# Link: https://github.com/magicmonty/bash-git-prompt +# Install: git clone https://github.com/magicmonty/bash-git-prompt.git ~/.bash-git-prompt --depth=1 +_SKIP_PROMPT_BASH_GIT_PROMPT=false + +# Bash Powerline (no need for patched fonts, supports git, previous command execution status, platform-dependent prompt symbols) +# Link: https://github.com/riobard/bash-powerline +# Install: curl https://raw.githubusercontent.com/riobard/bash-powerline/master/bash-powerline.sh > ~/.bash-powerline.sh +_SKIP_PROMPT_BASH_POWERLINE=false + +# Sexy Bash Prompt (supports git, 256 color) +# Link: https://github.com/twolfson/sexy-bash-prompt +# Install: (cd /tmp && ([[ -d sexy-bash-prompt ]] || git clone --depth 1 --config core.autocrlf=false https://github.com/twolfson/sexy-bash-prompt) && cd sexy-bash-prompt && make install) +_SKIP_PROMPT_SEXY_BASH_PROMPT=false + +# Liquid Prompt (adaptive prompt with low color and no glyphs) +# Link: https://github.com/nojhan/liquidprompt +# Install: git clone --branch stable https://github.com/nojhan/liquidprompt.git ~/liquidprompt +_SKIP_PROMPT_LIQUIDPROMPT=false + +# Original Powerline Status Line for Vim Bash Zsh fish tmux IPython Awesome i3 Qtile +# Link: https://github.com/powerline/powerline +# Install: https://medium.com/earlybyte/powerline-for-bash-6d3dd004f6fc +# NOTE: Requires Python and can be used with Trueline in Bash +# WARNING: This path may change or break in the future with new Python versions +_SKIP_PROMPT_POWERLINE=false + +# Recognize the depreciated _PROMPT_BUILTIN_FASTER_GIT option +if [[ ${_PROMPT_BUILTIN_FASTER_GIT} = true ]]; then + _GIT_IS_SLOW=true +fi +# END_BASHRC_CONFIG + +# Determine our kernel name +_KERNEL_NAME=$(printf '%.5s' "$(command uname -s)") + +####################################################### +# Add Common Binary Directories to Path +####################################################### + +# Add directories to the end of the path if they exist and are not already in the path +# Link: https://superuser.com/questions/39751/add-directory-to-path-if-its-not-already-there +function pathappend() { + for ARG in "$@"; do + if [[ -d "${ARG}" ]] && [[ ":${PATH}:" != *":${ARG}:"* ]]; then + PATH="${PATH:+"${PATH}:"}${ARG}" + fi + done +} + +# Add directories to the beginning of the path if they exist and are not already in the path +function pathprepend() { + for ((i = $#; i > 0; i--)); do + ARG="${!i}" + if [[ -d "${ARG}" ]] && [[ ":${PATH}:" != *":${ARG}:"* ]]; then + PATH="${ARG}${PATH:+":${PATH}"}" + fi + done +} + +# Append to an existing alias or otherwise create the new alias +aliasappend() { + # Display help if less than two arguments are provided + if [[ "$#" -lt 2 ]] || [[ "${1}" == "--help" ]] || [[ "${1}" == "-h" ]]; then + echo -e "${BRIGHT_CYAN}aliasappend${RESET}: Append an argument to an existing alias or create a new one" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}aliasappend${RESET} ${BRIGHT_MAGENTA}<${BRIGHT_YELLOW}alias_name${BRIGHT_MAGENTA}>${RESET} ${BRIGHT_MAGENTA}<${BRIGHT_YELLOW}argument${BRIGHT_MAGENTA}>${RESET}" + echo -e "${BRIGHT_WHITE}Examples:${RESET}" + echo -e " ${BRIGHT_CYAN}aliasappend${RESET} ${BRIGHT_YELLOW}grep '--color=auto'${RESET}" + return 2 + fi + + if alias "${1}" &>/dev/null; then + # Extract the alias value, ensuring it is correctly handled + local ALIAS_VALUE=$(alias "${1}" | sed -E "s/^alias ${1}='(.*)'$/\1/") + + # Check if the alias already contains the argument we want to append + if [[ "${ALIAS_VALUE}" != *"${2}"* ]]; then + # Correctly append the argument and redefine the alias with proper quoting + alias "${1}"="${ALIAS_VALUE} ${2}" + fi + else + # If the command is not an alias, create a new one with the provided argument + alias "${1}"="${1} ${2}" + fi +} + +# Add the most common personal binary paths located inside the home folder +# (these directories are only added if they exist) +pathprepend "${HOME}/bin" "${HOME}/sbin" "${HOME}/.local/bin" "${HOME}/local/bin" "${HOME}/.bin" + +# Check for the Rust package manager binary install location +# Link: https://doc.rust-lang.org/cargo/index.html +pathappend "${HOME}/.cargo/bin" "/root/.cargo/bin" "${HOME}/go/bin" + +# If the GOPATH environment variable is not set and the go command exists... +if hascommand --strict go && [[ -z ${GOPATH+x} ]] && [[ -d "${HOME}/go" ]]; then + # Set GOPATH to the default directory + export GOPATH="${HOME}/go" +fi + +####################################################### +# User Specific Environment Variables +####################################################### + +if [[ -f "${HOME}/.envrc" ]]; then + builtin source "${HOME}/.envrc" +fi + +if [[ -f "${HOME}/.env" ]]; then + builtin source "${HOME}/.env" +fi + +# Where the framework files are installed +# AUR/opt installs set this externally; standalone installs use the XDG default +BASHRC_INSTALL_DIR="${BASHRC_INSTALL_DIR:-${XDG_CONFIG_HOME:-${HOME}/.config}/bashrc}" + +# If installed via package manager but not yet configured, nudge the user +if [[ "${BASHRC_INSTALL_DIR}" == "/opt/"* ]] && [[ ! -f "${HOME}/.config/bashrc/.installed" ]]; then + echo "" + echo "Extreme Ultimate .bashrc is installed but not yet configured." + echo "Run: setup-bashrc" + echo "" + echo "To restore your original .bashrc instead:" + echo " mv ~/.bashrc.backup.* ~/.bashrc" + echo "" +fi + +if [[ -f "${BASHRC_INSTALL_DIR}/config" ]]; then + builtin source "${BASHRC_INSTALL_DIR}/config" +fi + +####################################################### +# Bashrc Support +####################################################### + +# Searches for text inside within the ~/.bashrc file +function findbashrc() { + # Display help if no arguments are provided + if [[ "$#" -eq 0 ]] || [[ "${1}" == "--help" ]] || [[ "${1}" == "-h" ]]; then + echo -e "${BRIGHT_CYAN}findbashrc${RESET}: Search for text inside ~/.bashrc" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}findbashrc${RESET} ${BRIGHT_MAGENTA}<${BRIGHT_YELLOW}pattern${BRIGHT_MAGENTA}>${RESET} ${BRIGHT_MAGENTA}[${BRIGHT_GREEN}pattern2${BRIGHT_MAGENTA}]${RESET} ${BRIGHT_MAGENTA}[${BRIGHT_GREEN}...${BRIGHT_MAGENTA}]${RESET}" + echo -e "${BRIGHT_WHITE}Examples:${RESET}" + echo -e " ${BRIGHT_CYAN}findbashrc${RESET} ${BRIGHT_YELLOW}'alias'${RESET} ${BRIGHT_BLUE}# Find all alias lines${RESET}" + echo -e " ${BRIGHT_CYAN}findbashrc${RESET} ${BRIGHT_YELLOW}'function' 'edit'${RESET} ${BRIGHT_BLUE}# Filter by multiple patterns${RESET}" + return 2 + fi + + # Start with the initial grep with a case-insensitive search + local RESULT="$(grep --color=always -n -i -e "$1" "${HOME}/.bashrc")" + + # Loop through all additional arguments to further filter the results + local ARGUMENT + for ARGUMENT in "${@:2}"; do + RESULT="$(echo "${RESULT}" | grep --color=always -i -e "${ARGUMENT}")" + done + + # Output the results + if [ -z "${RESULT}" ]; then + echo -e "${BRIGHT_RED}Error:${BRIGHT_CYAN} No matches found for specified patterns${RESET}" + return 1 + else + echo "${RESULT}" + fi +} + +# Opens ~/.bashrc in the default editor or searches it for provided patterns +alias ebrc='editbashrc' +function editbashrc() { + if [ $# -eq 0 ]; then + # If no arguments, open ~/.bashrc with the default editor + edit "${HOME}/.bashrc" + else + # Call findbashrc with all arguments passed to editbashrc + findbashrc "$@" + fi +} + +# Change into the bashrc directory +function bashrc() { + if [[ $# -eq 0 ]]; then + # Set the target directory + local TARGET_DIR="${BASHRC_INSTALL_DIR}" + + # Check if target directory exists + if [[ -d "${TARGET_DIR}" ]]; then + # Check for the presence of specific files or a non-empty subdirectory + if [[ -f "${TARGET_DIR}/aliases" ]] || \ + [[ -f "${TARGET_DIR}/bashhelp" ]] || \ + [[ -f "${TARGET_DIR}/config" ]] || \ + [[ -f "${TARGET_DIR}/help" ]] || \ + [[ -f "${TARGET_DIR}/README.md" ]] || \ + [[ -f "${TARGET_DIR}/README.html" ]] || \ + ( [[ -d "${TARGET_DIR}/bashrc.d" ]] && \ + [[ "$(ls -A "${TARGET_DIR}/bashrc.d")" ]] ); then + cd "${TARGET_DIR}" + else + cd "${HOME}" + fi + else + cd "${HOME}" + fi + else + # Call findbashrc with all passed parameters + findbashrc "$@" + fi +} + +# Create an alias to reload this .bashrc file +alias {rbrc,reloadbashrc,bashrcreload}='command clear; builtin source ~/.bashrc' + +# Find the help file for this .bashrc file (type hlp or press CONTROL-H) +if [[ -f "${BASHRC_INSTALL_DIR}/help" ]]; then + _BASHRC_HELP="${BASHRC_INSTALL_DIR}/help" + if [[ ${_SKIP_HELP_KEYBIND} == false ]]; then + bind -x '"\C-h":"less -f -r -n -S \"${_BASHRC_HELP}\""' + fi +elif [[ -f "${HOME}/.bashrc_help" ]]; then + _BASHRC_HELP="${HOME}/.bashrc_help" + if [[ ${_SKIP_HELP_KEYBIND} == false ]]; then + bind -x '"\C-h":"less -f -r -n -S \"${_BASHRC_HELP}\""' + fi +fi + +# Alias to edit the global bashrc if it exists using the same aliases upper case +if [[ -f "/etc/bash.bashrc" ]]; then + alias {EBRC,EDITBASHRC}='sudoedit /etc/bash.bashrc' +fi + +# Alias to show the help file +alias hlp='command less -f -r -n -S "${_BASHRC_HELP}"' + +# Exports a new config file for bashrc with environment variable settings +function bashrcnewconfig() { + # Help message remains the same + if [[ "$1" == "--help" ]] || [[ "$1" == "-h" ]]; then + echo -e "${BRIGHT_CYAN}bashrcnewconfig${RESET}: Export a new config file with environment variable settings" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}bashrcnewconfig${RESET} ${BRIGHT_MAGENTA}[${BRIGHT_GREEN}--test${BRIGHT_MAGENTA}]${RESET}" + echo -e "${BRIGHT_WHITE}Options:${RESET}" + echo -e " ${BRIGHT_GREEN}--test${RESET}, ${BRIGHT_GREEN}-t${RESET} Output config to stdout instead of file" + echo -e "${BRIGHT_WHITE}Examples:${RESET}" + echo -e " ${BRIGHT_CYAN}bashrcnewconfig${RESET} ${BRIGHT_BLUE}# Write to config file${RESET}" + echo -e " ${BRIGHT_CYAN}bashrcnewconfig${RESET} ${BRIGHT_GREEN}--test${RESET} ${BRIGHT_BLUE}# Preview in terminal${RESET}" + return 0 + fi + + # Determine if we're testing (outputting to stdout) or writing to file + local OUTPUT_TO_FILE=true + if [[ "$1" == "--test" ]] || [[ "$1" == "-t" ]] || [[ "$1" == "--testing" ]]; then + OUTPUT_TO_FILE=false + fi + + # Determine config path (only needed if writing to file) + local CONFIG_PATH + if ${OUTPUT_TO_FILE}; then + if [[ -f "${HOME}/.envrc" ]]; then + CONFIG_PATH="${HOME}/.envrc" + elif [[ -f "${HOME}/.env" ]]; then + CONFIG_PATH="${HOME}/.env" + else + mkdir -p "${BASHRC_INSTALL_DIR}" + CONFIG_PATH="${BASHRC_INSTALL_DIR}/config" + fi + + # Check if file exists and prompt for overwrite + if [[ -f "${CONFIG_PATH}" ]]; then + if ! ask "File ${CONFIG_PATH} already exists. Overwrite?" "N"; then + echo "Operation cancelled." + return 1 + fi + fi + fi + + # Function to write content (either to file or stdout) + write_content() { + if ${OUTPUT_TO_FILE}; then + cat > "${CONFIG_PATH}" + else + cat + fi + } + + # Generate the content + { + cat << 'EOL' +#!/usr/bin/env bash +####################################################### +# Extreme Ultimate .bashrc Environment Variables +# ~/.env +# ~/.envrc +# ~/.config/bashrc/config +# NOTE: Type env to see a list of set variables +####################################################### + +# Alias to edit this file +alias ebe="edit ${BASH_SOURCE}" + +####################################################### +# Set/override the default editor +# Examples: vim, nvim, emacs, nano, micro, helix, pico +# or gui apps like kate, geany, gedit, notepadqq, or vscodium +# NOTE: In Git Bash, you can use something like "/c/Program\ Files/Notepad++/notepad++.exe" +####################################################### +EOL + + # Extract and modify editor config section (between sentinel markers) + sed -n '/^# BEGIN_EDITOR_CONFIG$/,/^# END_EDITOR_CONFIG$/{//d;p;}' "${BASH_SOURCE}" | sed 's/^[[:space:]]*//' | while IFS= read -r LINE; do + if [[ "${LINE}" == *"export EDITOR"* ]] && [[ "${LINE}" != *"="* ]]; then + echo '# export EDITOR="nano"' + elif [[ "${LINE}" == *"export"* ]]; then + echo "# ${LINE}" + else + echo "${LINE}" + fi + done + + # Add separator + echo "#######################################################" + echo "# Extreme Ultimate .bashrc Configuration" + echo "#######################################################" + + # Extract and modify the config section (between sentinel markers) + sed -n '/^# BEGIN_BASHRC_CONFIG$/,/^# END_BASHRC_CONFIG$/{//d;p;}' "${BASH_SOURCE}" | while IFS= read -r LINE; do + if [[ "${LINE}" == "_BDAY_FILE="* ]]; then + echo "# _BDAY_FILE=" + else + echo "${LINE}" + fi + done + } | write_content + + if ${OUTPUT_TO_FILE}; then + echo "Configuration exported successfully to: ${CONFIG_PATH}" + fi +} + +####################################################### +# Use these commands to keep the .bashrc immutable and write protected +# even from root so that other scripts and applications can't change it +####################################################### + +alias bashrcprotect="sudo chattr +i ${HOME}/.bashrc" +alias bashrcunprotect="sudo chattr -i ${HOME}/.bashrc" +alias bashrccheckprotect='if [[ $(lsattr -R -l ~/.bashrc | grep " Immutable") ]]; then echo "Protected"; else echo "Not Protected"; fi;' + +####################################################### +# Bashrc Updates +####################################################### + +# Update the main .bashrc file and supporting files +alias bashrcupdateforce="download 'https://sourceforge.net/projects/ultimate-bashrc/files/_bashrc/download' '${HOME}' && command mv --interactive '${HOME}/_bashrc' '${HOME}/.bashrc'" +function bashrcupdate() { + + # If installed via package manager, redirect to the package manager + if [[ "${BASHRC_INSTALL_DIR}" == "/opt/"* ]]; then + echo -e "${BRIGHT_YELLOW}This installation is managed by your package manager.${RESET}" + if hascommand yay; then + echo -e "${BRIGHT_YELLOW}Run: ${BRIGHT_CYAN}yay -Syu extreme-ultimate-bashrc${RESET}" + elif hascommand paru; then + echo -e "${BRIGHT_YELLOW}Run: ${BRIGHT_CYAN}paru -Syu extreme-ultimate-bashrc${RESET}" + else + echo -e "${BRIGHT_YELLOW}Run: ${BRIGHT_CYAN}pacman -Syu extreme-ultimate-bashrc${RESET}" + fi + return 0 + fi + + # Files to update + declare -A FILES_TO_UPDATE=( + ["${HOME}/.bashrc"]="https://sourceforge.net/projects/ultimate-bashrc/files/_bashrc/download" + ["${HOME}/.bashrc_help"]="https://sourceforge.net/projects/ultimate-bashrc/files/_bashrc_help/download" + ["${BASHRC_INSTALL_DIR}/help"]="https://sourceforge.net/projects/ultimate-bashrc/files/_bashrc_help/download" + ["${HOME}/README.md"]="https://sourceforge.net/projects/ultimate-bashrc/files/README.md/download" + ["${BASHRC_INSTALL_DIR}/README.md"]="https://sourceforge.net/projects/ultimate-bashrc/files/README.md/download" + ["${HOME}/README.html"]="https://sourceforge.net/projects/ultimate-bashrc/files/README.html/download" + ["${BASHRC_INSTALL_DIR}/README.html"]="https://sourceforge.net/projects/ultimate-bashrc/files/README.html/download" + ) + + if ! hascommand axel && ! hascommand aria2c && ! hascommand curl && ! hascommand wget; then + if hascommand --strict xdg-open; then + xdg-open "https://sourceforge.net/projects/ultimate-bashrc/files/" > /dev/null 2>&1 & disown + elif hascommand --strict open; then # For macOS + open "https://sourceforge.net/projects/ultimate-bashrc/files/" > /dev/null 2>&1 & disown + elif hascommand --strict start; then # For Windows + start "" "https://sourceforge.net/projects/ultimate-bashrc/files/" + else + echo -e "${BRIGHT_YELLOW}Please install either wget, curl, aria2, or visit ${BRIGHT_CYAN}https://sourceforge.net/projects/ultimate-bashrc/${BRIGHT_YELLOW} to update.${RESET}" + fi + return + fi + + # Backup the existing ~/.bashrc with a time date stamp + local BASHRC_BACKUP_FILE="${HOME}/.bashrc.$(date +"%Y-%m-%d_%H-%M-%S").backup" + echo -e "${BRIGHT_YELLOW}Backing up your .bashrc file to ${BRIGHT_CYAN}${BASHRC_BACKUP_FILE}${RESET}" + command cp "${HOME}/.bashrc" "${BASHRC_BACKUP_FILE}" || { + echo -e "${BRIGHT_RED}Backup of .bashrc failed. Please check your permissions and disk space.${RESET}" + return 1 + } + + # Create a temporary directory for downloads + # NOTE: Using _UPDATE_TMPDIR to avoid shadowing the global $TMPDIR env var + local _UPDATE_TMPDIR=$(mktemp -d) + if [[ ! -d "${_UPDATE_TMPDIR}" ]] || [[ -z "${_UPDATE_TMPDIR}" ]]; then + echo -e "${BRIGHT_RED}Failed to create a temporary directory. Check your system's temporary storage settings.${RESET}" + return 1 + fi + # Use RETURN trap (not EXIT) so cleanup happens when the function returns, + # not when the entire shell exits (when local vars are out of scope) + trap 'rm -rf "${_UPDATE_TMPDIR}"' RETURN + + # Loop through each file + for FILE in "${!FILES_TO_UPDATE[@]}"; do + + # Get the download URL + local URL="${FILES_TO_UPDATE[${FILE}]}" + + # If the file exists or it's the .bashrc which we will always update... + if [[ -f "${FILE}" ]] || [[ "$(command basename "${FILE}")" == ".bashrc" ]]; then + + # Remove immutable attribute if set + local IS_IMMUTABLE=false + if hascommand lsattr && hascommand chattr && command lsattr -R -l "${FILE}" 2>/dev/null | command grep -q "Immutable"; then + echo -e "${BRIGHT_YELLOW}Removing immutable attribute from ${FILE}${RESET}" + sudo chattr -i "${FILE}" + IS_IMMUTABLE=true + fi + + # Ensure temporary directory is empty + command rm -f "${_UPDATE_TMPDIR}"/* + + # Download to temporary directory + echo "Updating ${FILE}" + download "${URL}" "${_UPDATE_TMPDIR}" || { + echo -e "${BRIGHT_RED}Failed to download ${FILE}${RESET}" + continue + } + + # Now find the file that was just downloaded + local DOWNLOADED_FILE=$(command find "${_UPDATE_TMPDIR}" -type f -exec ls -1t {} + | command head -n1) + + # Move downloaded file to the desired location + if [[ -f "${DOWNLOADED_FILE}" ]]; then + command mv "${DOWNLOADED_FILE}" "${FILE}" + echo -e "${BRIGHT_GREEN}Updated ${FILE} successfully${RESET}" + else + echo -e "${BRIGHT_RED}Download failed or file not found for ${FILE}${RESET}" + fi + + # If this is the .bashrc file, make sure it does not contain errors! + if [[ "$(command basename "${FILE}")" == ".bashrc" ]]; then + bash -n "${HOME}/.bashrc" + if [ $? -ne 0 ]; then + echo -e "${BRIGHT_RED}New .bashrc contains errors, reverting backup.${RESET}" + command mv "${BASHRC_BACKUP_FILE}" "${HOME}/.bashrc" + return 1 + fi + fi + + # Restore the immutable flag if it was removed + if [[ "${IS_IMMUTABLE}" == true ]]; then + echo -e "${BRIGHT_YELLOW}Restoring immutable attribute to ${FILE}${RESET}" + sudo chattr +i "${FILE}" + fi + fi + done + + echo -e "${BRIGHT_GREEN}Done!${RESET} ${BRIGHT_MAGENTA}Restart your terminal to see the changes.${RESET}" +} + +####################################################### +# Change the default file and directory permissions for newly created files +# in the home directory for security (also see fixuserhome function) +####################################################### + +# Only if we are not the root user... +if [[ ${_SKIP_UMASK_HOME} == false ]] && [[ ${EUID} -ne 0 ]]; then + # Set to umask 027 while leaving execute for directories + # - Grant full permissions to the user (owner) + # - Grant read and execute permissions to the group + # - Remove all permissions for others + umask u=rwx,g=rx,o= +fi + +####################################################### +# General Aliases +# NOTE: To temporarily bypass an alias, we proceed the command with a \ +# EG: if the ls command is aliased, to use the normal command you would type \ls +####################################################### + +# Find text in man pages +alias findman='apropos' + +# Find environment variables +alias findenv='printenv | grep -i' + +# List available aliases with optional filter parameter +function findalias() { + # Help text + if [[ "${1}" == "--help" ]] || [[ "${1}" == "-h" ]]; then + echo -e "${BRIGHT_CYAN}findalias${RESET}: Search through defined aliases" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}findalias${RESET} ${BRIGHT_MAGENTA}[${BRIGHT_GREEN}pattern${BRIGHT_MAGENTA}]${RESET}" + echo -e "${BRIGHT_WHITE}Examples:${RESET}" + echo -e " ${BRIGHT_CYAN}findalias${RESET} ${BRIGHT_BLUE}# List all aliases${RESET}" + echo -e " ${BRIGHT_CYAN}findalias${RESET} ${BRIGHT_GREEN}git${RESET} ${BRIGHT_BLUE}# Find git-related aliases${RESET}" + echo -e " ${BRIGHT_CYAN}findalias${RESET} ${BRIGHT_GREEN}'ls|ll'${RESET} ${BRIGHT_BLUE}# Regex pattern${RESET}" + return 0 + fi + + # Assign the first argument to FILTER for filtering the output + local FILTER="${1}" + + # Print the section heading for aliases + echo -e "${BRIGHT_RED}Aliases:${RESET}" + + # List all aliases, format and color their output, then apply the filter + alias | awk -F'[ =]' '{print "\033[33m"$2"\033[0m\t\033[34m"$0"\033[0m";}' | grep -E "${FILTER}" +} + +# List available functions with optional filter parameter +function findfunction() { + # Help text + if [[ "${1}" == "--help" ]] || [[ "${1}" == "-h" ]]; then + echo -e "${BRIGHT_CYAN}findfunction${RESET}: Search through defined functions" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}findfunction${RESET} ${BRIGHT_MAGENTA}[${BRIGHT_GREEN}pattern${BRIGHT_MAGENTA}]${RESET}" + echo -e "${BRIGHT_WHITE}Examples:${RESET}" + echo -e " ${BRIGHT_CYAN}findfunction${RESET} ${BRIGHT_BLUE}# List all functions${RESET}" + echo -e " ${BRIGHT_CYAN}findfunction${RESET} ${BRIGHT_GREEN}git${RESET} ${BRIGHT_BLUE}# Find git-related functions${RESET}" + echo -e " ${BRIGHT_CYAN}findfunction${RESET} ${BRIGHT_GREEN}find${RESET} ${BRIGHT_BLUE}# Find search functions${RESET}" + return 0 + fi + + # Assign the first argument to FILTER for filtering the output + local FILTER="${1}" + + # Print the section heading for functions + echo -e "${BRIGHT_RED}Functions:${RESET}" + + # List all user-defined functions, filter out private ones starting with + # an underscore, and apply the filter + compgen -A function | grep -v '^_.*' | grep -E "${FILTER}" +} + +# Show a list of available aliases and functions with optional filtering +function a() { + # Store the first argument to FILTER + local FILTER="${1}" + + # Combine the output of aliases and functions with optional filtering + { findalias "${FILTER}"; echo; findfunction "${FILTER}"; } | command less --line-numbers --no-init +} + +####################################################### +### DIRECTORY ALIASES +####################################################### + +# This allows you to bookmark your favorite places across the file system +# Define a variable containing a path and you will be able to cd into it +# regardless of the directory you're in like this: +# export desktop="${HOME}/Desktop" +# cd desktop +shopt -s cdable_vars + +# Declare an associative array for directory locations (with alternatives) +# NOTE: These aliases are case sensitive where lower case is the local user +# directory and upper case is the global system directory +declare -A ALIASES=( + ["autostart"]="${XDG_CONFIG_HOME:-${HOME}/.config}/autostart" + ["bin"]="${HOME}/.local/bin" + ["BIN"]="/usr/bin" + ["cache"]="${XDG_CACHE_HOME:-${HOME}/.cache}" + ["config"]="${XDG_CONFIG_HOME:-${HOME}/.config}" + ["CONFIG"]="/etc" + ["desktop"]="$(command -v xdg-user-dir > /dev/null && xdg-user-dir DESKTOP || echo "${HOME}/Desktop")" + ["docs"]="$(command -v xdg-user-dir > /dev/null && xdg-user-dir DOCUMENTS || echo "${HOME}/Documents")" + ["documents"]="$(command -v xdg-user-dir > /dev/null && xdg-user-dir DOCUMENTS || echo "${HOME}/Documents")" + ["DOCS"]="/usr/local/man /usr/local/share/man /usr/share/man" + ["downloads"]="$(command -v xdg-user-dir > /dev/null && xdg-user-dir DOWNLOAD || echo "${HOME}/Downloads")" + ["fonts"]="${XDG_DATA_HOME:-${HOME}/.local/share}/fonts ${HOME}/.fonts" + ["FONTS"]="/usr/share/fonts" + ["icons"]="${XDG_DATA_HOME:-${HOME}/.local/share}/icons ${HOME}/.icons" + ["ICONS"]="/usr/share/icons" + ["music"]="$(command -v xdg-user-dir > /dev/null && xdg-user-dir MUSIC || echo "${HOME}/Music")" + ["pics"]="$(command -v xdg-user-dir > /dev/null && xdg-user-dir PICTURES || echo "${HOME}/Pictures")" + ["pictures"]="$(command -v xdg-user-dir > /dev/null && xdg-user-dir PICTURES || echo "${HOME}/Pictures")" + ["share"]="${XDG_DATA_HOME:-${HOME}/.local/share}" + ["SHARE"]="/usr/share" + ["shortcuts"]="${XDG_DATA_HOME:-${HOME}/.local/share}/applications ${HOME}/.gnome/apps" + ["SHORTCUTS"]="/usr/share/applications /usr/local/share/applications" + ["themes"]="${HOME}/.themes ${XDG_DATA_HOME:-${HOME}/.local/share}/themes" + ["THEMES"]="/usr/share/themes" + ["tmp"]="${HOME}/tmp ${XDG_CACHE_HOME:-${HOME}/.cache}/tmp ${XDG_CACHE_HOME:-${HOME}/.cache}" + ["TMP"]="${TMPDIR:-/tmp}" + ["videos"]="$(command -v xdg-user-dir > /dev/null && xdg-user-dir VIDEOS || echo "${HOME}/Videos")" + ["wallpaper"]="${XDG_DATA_HOME:-${HOME}/.local/share}/wallpapers" + ["WALLPAPER"]="/usr/share/backgrounds /usr/share/wallpapers" + ["web"]="/srv/http /var/www/html /usr/share/nginx/html /opt/lampp/htdocs /usr/local/apache2/htdocs /usr/local/www/apache24/data" +) + +# Save original IFS and set it to space for parsing directories +OLD_IFS="${IFS}" +IFS=' ' + +# Loop through the associative array and create aliases and exports for existing directories +for ALIAS in "${!ALIASES[@]}"; do + DIRECTORIES=(${ALIASES[${ALIAS}]}) + for DIRECTORY in "${DIRECTORIES[@]}"; do + if [[ -d "${DIRECTORY}" ]]; then + alias "${ALIAS}"="cd \"${DIRECTORY}\"" + export "${ALIAS}"="${DIRECTORY}" + break # Only set the first found directory + fi + done +done + +# Restore original IFS +IFS="${OLD_IFS}" + +# Clean up +unset OLD_IFS DIRECTORY DIRECTORIES ALIAS ALIASES + +####################################################### +### GIT ALIASES +####################################################### + +# If git is installed... +if hascommand --strict git; then + + # Git Alias: Provides many useful Git alias commands + # This alias will install/update the Git alias commands file + # Link: https://github.com/GitAlias/gitalias + # Edit ~/.gitconfig and then include the path to this file like this: + # [include] + # path = gitalias.txt + alias gitalias='curl -L --output "${HOME}/gitalias.txt" https://raw.githubusercontent.com/GitAlias/gitalias/main/gitalias.txt && git config --global include.path "gitalias.txt"' + + # When invoked without arguments gg will do a short Git status, + # otherwise it will just pass on the given arguments to the Git command. + # Status is likely to be the Git command one will execute the most, + # hence this simple enhancement does prove very useful in practice. + # - `??`: Untracked file (not in the repository) + # - `A`: Added to the index (staged for commit) + # - `M`: Modified (changes not staged for commit) + # - `D`: Deleted (deleted but not staged for commit) + # - `R`: Renamed (file has been renamed, not staged for commit) + # - `C`: Copied (file has been copied, not staged for commit) + # - `U`: Unmerged (conflict in merging changes) + # - `!!`: Ignored (file is ignored by Git) + # - `1`: Index and working tree match (status for submodules) + # - `2`: Working tree changed but index not updated (status for submodules) + # - `?`: Unknown (an error occurred while trying to obtain the status) + # git status --short --branch + function gg() { + # Check if the number of arguments is zero + if [[ $# -eq 0 ]]; then + # Call gitls when no arguments are provided + gitls + else + # Pass all arguments to the 'git' command + git "$@" + fi + } + + # All Git aliases start with gg for speed + alias ggg='git status' # Standard git status + alias ggs='git status --short --branch' # Compact status with branch info + alias ggf='git fetch' # Fetch changes from remote + alias ggprune='git fetch --prune' # Remove references to deleted remote branches + alias ggp='git pull' # Fetch and merge changes + alias ggpu='git push' # Push changes to remote + alias ggm='git merge' # Merge branches + alias ggri='git rebase --interactive' # Start interactive rebase + alias ggsquash='git reset $(git merge-base $(git symbolic-ref refs/remotes/origin/HEAD | sed "s@^refs/remotes/origin/@@") $(git branch --show-current)) && \ + git add --all && \ + git commit --message "Squashed commits"' # Reset to default branch base and squash all commits + + # Branch operations + alias ggb='gitbranch' # Change branch (custom function) + alias gg-='git checkout -' # Switch to previous branch + alias ggback='git checkout HEAD~' # Go back one commit + alias ggcb='git checkout -b' # Create new branch + alias ggcbforce='git checkout -B' # Create/reset branch + alias ggbdelete='ask "Are you sure you want to delete this branch? This cannot be undone." N || { echo "Canceled"; false; } && git branch -D' # Safe branch deletion + alias {gglb,ggbl}='git for-each-ref --sort=-committerdate refs/heads/ \ + --format="%(committerdate:short) %(refname:short)" | head -n 25' # List branches sorted by last commit date + alias ggsb='git for-each-ref --format="%(refname:short)" refs/heads refs/remotes | sed "s/origin\///" | sort -u | grep -i' # Search branch names + + # Staging and committing + alias gga='git add' # Stage files + alias ggaa='git add --patch' # Interactive staging + alias ggac='git add --all && git commit --verbose --message' # Stage all and commit + alias ggua='git reset' # Unstage files + alias ggundo='git reset --soft HEAD~1' # Undo last commit, keep changes staged + alias ggr='git restore' # Restore files to HEAD + alias ggrm='git rm' # Remove and stage deletion + alias ggc='git commit --verbose -m' # Commit with message + alias ggca='git commit --amend --verbose' # Modify last commit + + # Searching and diffing + alias ggfind='git log --pretty=format:"%C(yellow)%h%Cred%d %Creset%s%Cblue [%cn]" --grep' # Search commit messages + alias ggd='git diff' # Show unstaged changes + alias ggdc='git diff --cached' # Show staged changes + alias ggdd='git remote update > /dev/null 2>&1 && \ + git status -uno | grep -q "Your branch is behind" && \ + git fetch origin; \ + git diff origin/$(git symbolic-ref refs/remotes/origin/HEAD | sed "s@^refs/remotes/origin/@@")..HEAD' # Compare with default branch + + alias ggds='git diff --stat' # Show changed files stats + + # Stash operations + alias ggst='git stash' # Stash changes + alias ggsta='git stash --patch' # Interactive stash + alias ggpop='git stash pop' # Apply stashed changes + alias ggstlist='git stash list' # List stashes + + # Repository information + alias ggls='git ls-files' # List all tracked files + alias ggrv='git remote --verbose' # List remote repositories + alias gglast='git show --stat' # Show last commit changes + + # Git alias for log tailored for local projects or office use + # Reference for Git Pretty Formats: + # - %h: Abbreviated commit hash + # - %ad: Author date (format can be customized) + # - %s: Commit summary + # - %b: Body of the commit message. + # - %an: Author name + # - %ae: Author email + # - %cn: Committer's name (the person who applied the commit) + # - %ce: Committer's email + # - %d: Ref names (branches or tags associated with the commit) + # - %H: Commit hash + # - %h: Abbreviated commit hash + # - %p: Parent hashes of a commit (useful for merge commits) + # - %G?: GPG signature status code + # - %GS: GPG signature’s signer + alias ggl='git log --oneline --date=short --decorate --pretty=format:"%C(magenta)%h%C(reset) - %C(green)(%cr)%C(reset) %s%C(auto)%d%C(reset) %C(bold brightblue)<%an>%C(reset)"' + + # Git alias for more detailed list log that is better for online collaberation + # The last 1 character field is the GPG signature for the commit + # - G: Good signature + # - B: Bad signature + # - U: Good signature with unknown validity + # - X: Good signature that has expired + # - Y: Good signature made by an expired key + # - R: Good signature made by a revoked key + # - E: Signature cannot be checked (e.g., missing key) + # - N: No signature + alias ggll='git log --graph --topo-order --date=iso8601-strict --no-abbrev-commit --decorate --all --boundary --pretty=format:"%C(green)%ad %C(magenta)%h%Creset -%C(auto)%d%Creset %s %C(brightblue)[%aN <%aE>]%C(cyan)(%cN)%C(reset) %C(brightyellow)%G?%C(reset)"' + + # Vacuum the local Git repository database, reduce it's size, and clean out logs + # Link: https://stackoverflow.com/questions/2116778/reduce-git-repository-size + alias ggclean='git reflog expire --all --expire=now && git gc --prune=now --aggressive' + + # Check PHP syntax on all modified and untracked PHP files in the current branch + # Useful for catching syntax errors before committing + function phpcheckbranch() { + # Get all modified PHP files (staged + unstaged changes) + local PHP_FILES=$(git diff --name-only HEAD 2>/dev/null | grep '\.php$') + + # Get all untracked PHP files (new files not yet added to git) + local UNTRACKED=$(git ls-files --others --exclude-standard 2>/dev/null | grep '\.php$') + + # Combine modified and untracked files into one list + if [[ -n "${UNTRACKED}" ]]; then + if [[ -n "${PHP_FILES}" ]]; then + PHP_FILES=$(printf "%s\n%s" "${PHP_FILES}" "${UNTRACKED}") + else + PHP_FILES="${UNTRACKED}" + fi + fi + + # Exit early if no PHP files found + if [[ -z "${PHP_FILES}" ]]; then + echo -e "${BRIGHT_YELLOW}No modified PHP files found${RESET}" + return 0 + fi + + # Track results + local FAILED=0 + local TOTAL=0 + + # Check each PHP file using the phpcheck function + while IFS= read -r FILE; do + if [[ -f "${FILE}" ]]; then + ((TOTAL++)) + phpcheck "${FILE}" + if [[ $? -ne 0 ]]; then + ((FAILED++)) + fi + echo "" # Add spacing between file checks + fi + done <<< "${PHP_FILES}" + + # Print summary + echo -e "${BRIGHT_CYAN}════════════════════════════════${RESET}" + if [[ ${FAILED} -gt 0 ]]; then + echo -e "${BRIGHT_RED}✗ Failed: ${FAILED}/${TOTAL} file(s)${RESET}" + return 1 + else + echo -e "${BRIGHT_GREEN}✓ All ${TOTAL} PHP file(s) passed!${RESET}" + return 0 + fi + } + + # Find all Git repos in the current directory recursively + # Note: Excludes hidden and temp directories + # You can find all git repos on the entire system using: locate .git | grep "/.git$" + alias gitrepos="find . -type d -not -path '*/.git/*' -not -path '*/tmp/*' -not -path '*/temp/*' -not -path '*/.*' -exec test -e '{}/.git' ';' -print -prune" + + # Git Auto-Completion + # Link: https://github.com/git/git/tree/master/contrib/completion + # Install: wget -O ~/git-completion.bash https://github.com/git/git/raw/master/contrib/completion/git-completion.bash + if [[ -f "${HOME}/git-completion.bash" ]]; then + builtin source "${HOME}/git-completion.bash" + fi + + # If eza with Git support is installed... + # Link: https://eza.rocks + if hascommand --strict eza; then + + # If Git is not slow, always use the eza Git repo flag globally + if [[ ${_GIT_IS_SLOW} == false ]]; then + alias {eza,exa}='command eza --git' + else + alias {eza,exa}='command eza --git --git-repos-no-status' + fi + + if [[ ${_SKIP_EXA} == false ]]; then + alias lsg='eza --long --all --links --group --modified --classify --group-directories-first --color=auto --color-scale' + fi + + # If exa with Git support is installed... + # Link: https://github.com/ogham/exa + elif hascommand --strict exa; then + + # Add Git support to Exa only if Git is not slow + if [[ ${_GIT_IS_SLOW} == false ]]; then + alias exa='command exa --git' + fi + + if [[ ${_SKIP_EXA} == false ]]; then + alias lsg='exa --long --all --links --group --modified --classify --group-directories-first --color=auto --color-scale' + fi + fi + + # If lsd with Git support is installed... + # Link: https://github.com/Peltoche/lsd + if hascommand --strict lsd; then + # Add icons if unicode and the icon paramter is supported + if [[ -n $(command lsd --help | grep -e '--git\s' 2> /dev/null) ]]; then + alias lsd='command lsd --git' + fi + fi + + # Nearly everything you can do in Git but in a terminal UI + # Link: https://github.com/Extrawurst/gitui + if hascommand gitui; then + alias ggu='gitui' + fi + + # Tig ncurses-based text-mode interface for git + # Link: https://jonas.github.io/tig/ + if hascommand tig; then + alias gitt='tig' + fi + + # Git Commander + # Link: https://github.com/golbin/git-commander + # Install: npm install -g git-commander + if hascommand git-commander; then + alias gitc='git-commander' + fi + + # GRV - Git Repository Viewer + # Link: https://github.com/rgburke/grv + if hascommand grv; then + alias gitrv='grv' + fi + + # LazyGit - Terminal UI for git commands + # Link: https://github.com/jesseduffield/lazygit + if hascommand lazygit; then + alias lg='lazygit' + fi + + # Ugit - Git Undo + # Link: https://github.com/Bhupesh-V/ugit + # Install: sh -c "$(curl -fsSL https://raw.githubusercontent.com/Bhupesh-V/ugit/master/install)" + if hascommand ugit; then + alias {gitundo,ggundo}='ugit' + fi + + # List Git files by last modified date + # Link: https://stackoverflow.com/questions/14141344/git-status-list-last-modified-date + alias gitmodifieddate='IFS=""; git status -s | while read -n2 mode; read -n1; read file; do echo "$mode" "$(stat -c %y "$file")" "$file"; done | sort -k1,4' + + # Returns you to the Git project's top level + alias cg='cd "$(git rev-parse --show-toplevel)"' + + # Displays git status with human-readable descriptions aligned in columns + function gitls() { + local COUNT=0 # Will keep the file count + local TOGGLE=0 # Toggle variable for alternating row colors + + # Getting the current branch name + local CURRENT_BRANCH + CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null) + if [[ $? -ne 0 ]]; then + echo -e "${BRIGHT_RED}Error: ${BRIGHT_CYAN}Not a git repository ${BRIGHT_MAGENTA}(or any parent up to mount point /)${RESET}" + return 1 # Return with an error status code + fi + echo -e "${BRIGHT_RED}Current branch: ${BRIGHT_WHITE}${CURRENT_BRANCH}${RESET}" + + # Storing the output of git status --porcelain in a variable + local GIT_STATUS=$(git status --porcelain | sort -k1,1 -k2) + if [[ -z "${GIT_STATUS}" ]]; then + echo -e "${BRIGHT_CYAN}No changes found in the repository${RESET}" + return 0 # Return with a success status code + fi + + # Find the maximum length of the filenames + local MAX_FILENAME_LENGTH=0 + local FILENAME FILENAME_LENGTH + while IFS= read -r LINE; do + FILENAME="${LINE:2}" + FILENAME_LENGTH=${#FILENAME} + # Compare the current filename length with the current max + MAX_FILENAME_LENGTH=$((MAX_FILENAME_LENGTH < FILENAME_LENGTH ? FILENAME_LENGTH : MAX_FILENAME_LENGTH)) + done <<< "${GIT_STATUS}" + + # Cap the MAX_FILENAME_LENGTH at 80 if it exceeds it + if [[ ${MAX_FILENAME_LENGTH} -gt 80 ]]; then + MAX_FILENAME_LENGTH=80 + fi + + # Running git status with the porcelain flag + local CODE DESCRIPTION COLOR BACKGROUND + while IFS= read -r LINE; do + # Count the files + ((COUNT++)) + + # Extracting the status code and the file name + CODE="${LINE:0:2}" + FILENAME="${LINE:2}"; FILENAME="${FILENAME# }" + + # Matching the status code with the corresponding description + DESCRIPTION="" + case "${CODE}" in + "??") DESCRIPTION="Untracked file (not in the repository)";; + "A ") DESCRIPTION="Added (staged for commit)";; + "AM") DESCRIPTION="Added (staged) with unstaged modifications";; + "AD") DESCRIPTION="Added (staged) then deleted in working tree";; + "M ") DESCRIPTION="Modified (staged for commit)";; + "MM") DESCRIPTION="Modified (staged) with additional unstaged modifications";; + " M") DESCRIPTION="Modified (changes not staged for commit)";; + "D ") DESCRIPTION="Deleted (staged for commit)";; + " D") DESCRIPTION="Deleted (not staged for commit)";; + "R ") DESCRIPTION="Renamed (staged for commit)";; + "C ") DESCRIPTION="Copied (staged for commit)";; + "T ") DESCRIPTION="Type changed (staged for commit)";; + "U ") DESCRIPTION="Unmerged (conflict in merging changes)";; + "UU") DESCRIPTION="Unmerged (both modified -- merge conflict)";; + "AA") DESCRIPTION="Unmerged (both added -- merge conflict)";; + "DD") DESCRIPTION="Unmerged (both deleted -- merge conflict)";; + "!!") DESCRIPTION="Ignored (file is ignored by Git)";; + *) DESCRIPTION="Status: ${CODE}";; + esac + + # Determine if the file is a directory + if [[ -d "${FILENAME}" ]]; then + COLOR="${BRIGHT_BLUE}" + else + COLOR="${BRIGHT_YELLOW}" + fi + + # Applying alternating background colors + if [[ ${TOGGLE} -eq 0 ]]; then + BACKGROUND="\033[100m" # Dark grey background + TOGGLE=1 + else + BACKGROUND="\033[49m" # Default background + TOGGLE=0 + fi + + # Printing the filename and description with the chosen colors + printf "${BACKGROUND}${COLOR}%-*s${BRIGHT_CYAN}%s${RESET}\n" "${MAX_FILENAME_LENGTH}" "${FILENAME}" " ${DESCRIPTION}" + + # Loop through the next git status line + done <<< "${GIT_STATUS}" + + # Printing the total count in purple and green + echo -e "${MAGENTA}Total files:${RESET} ${COUNT}" + } + + # Update all Git repositories in the specified directory with depth as a parameter + function gitupdaterepos() { + # Set the depth level for the search with 1 as default if not specified + local DEPTH_LEVEL=${1:-1} + + # Check if the necessary commands (git, sed, sort) are available in the environment + if hascommand git && hascommand sed && hascommand sort; then + # Save the current directory to a variable + local CURRENT_DIR=$(command pwd) + + # Alert the user to the current directory and the action to be taken + echo -e "${BRIGHT_YELLOW}=>${RESET} You are about to update Git repositories in: ${CURRENT_DIR}" + echo -e "${BRIGHT_BLUE}=>${RESET} Listed repositories:" + + # Use IFS and readarray to handle spaces in file paths properly + local IFS=$'\n' + local REPOS=() + readarray -t REPOS < <(command find "${CURRENT_DIR}" -mindepth 1 -maxdepth $((DEPTH_LEVEL + 1)) -type d -name '.git' | command sed 's|/.git$||' | command sort -u) + + # Display the directories to be updated + local REPO + for REPO in "${REPOS[@]}"; do + echo "${REPO}" + done + + # Prompt the user for confirmation to proceed with updating + local RESPONSE + read -r -p "Are you sure you want to proceed? (y/N): " RESPONSE + if [[ "${RESPONSE,,}" != "y" ]]; then + echo -e "${BRIGHT_RED}=>${RESET} Update canceled" + return + fi + + # Iterate over each repository directory and attempt a 'git pull' operation + local REPO_DIR + for REPO_DIR in "${REPOS[@]}"; do + builtin cd "${REPO_DIR}" || { echo -e "${BRIGHT_RED}=>${RESET} Error entering ${REPO_DIR}"; continue; } + echo -e "${BRIGHT_YELLOW}=>${RESET} Updating Git repo in ${REPO_DIR}..." + + # Execute a git pull and display success or failure messages + git pull && echo -e "${BRIGHT_GREEN}=>${RESET} Successfully updated ${REPO_DIR}" || echo -e "${BRIGHT_RED}=>${RESET} Failed to update ${REPO_DIR}" + + # Return to the original directory after each update + builtin cd - > /dev/null + done + else + # If required commands are not available, alert the user + echo -e "${BRIGHT_RED}=>${RESET} Required commands Git, sed, and/or sort are not installed" + fi + } + + # Change the Git branch - If no branch is specified as an argument, then + # the user is prompted to select from a list the available branches + # Syntax: gitbranch [optional_branch_name] + function gitbranch() { + # Check if the current directory is a Git repository + if git rev-parse --is-inside-work-tree > /dev/null 2>&1; then + # No arguments passed + if [[ $# -eq 0 ]]; then + # Check if there is a remote server + if git remote -v | grep -q 'origin'; then + # Prompt the user for action + if ask "${BRIGHT_WHITE}Download ${BRIGHT_YELLOW}remote${BRIGHT_WHITE} branch names?${BRIGHT_RED} Could be slow for large repos.${RESET}" N; then + # Fetch latest remote branches; handle errors + git fetch origin 2>&1 + local RAW_REMOTE_OUTPUT REMOTE_BRANCHES + RAW_REMOTE_OUTPUT=$(git ls-remote --refs --sort=-committerdate origin 2>&1) + if [[ $? -ne 0 ]]; then + echo -e "${BRIGHT_RED}Error: ${BRIGHT_CYAN}${RAW_REMOTE_OUTPUT}${RESET}" + return 1 + fi + REMOTE_BRANCHES=$(echo "${RAW_REMOTE_OUTPUT}" | awk '{sub("refs/heads/", ""); print $2}') + # Use createmenu for selection + git checkout "$(echo "${REMOTE_BRANCHES}" | createmenu)" + else + # Use local branches for selection via createmenu + git checkout "$(git branch --sort=-committerdate | cut -c 3- | createmenu)" + fi + else + # No remote server, use local branches only + git checkout "$(git branch --sort=-committerdate | cut -c 3- | createmenu)" + fi + else + # Argument passed, just switch to that branch + git checkout "${@}" + fi + else + # Not a Git repo + echo -e "${BRIGHT_RED}ERROR: ${BRIGHT_CYAN}Current directory is not a Git repository${RESET}" + fi + } + + # Forces Git to overwrite local files and resets the branch (or master) + # Important: If you have any local changes, they will be lost (if they're tracked)! + function gitresetbranch() { + # Make sure we are in a Git repo + if ! git rev-parse --git-dir > /dev/null 2>&1; then + echo -e "${BRIGHT_RED}ERROR: ${BRIGHT_CYAN}Current directory is not a Git repository${RESET}" + return 1 + fi + + # Default to current branch if not specified + local BRANCH="${1:-$(git rev-parse --abbrev-ref HEAD)}" + + # Check if a branch name was provided, and confirm if using the current branch + if [ -z "${BRANCH}" ]; then + echo -e "${BRIGHT_RED}Operation cancelled.${RESET} ${BRIGHT_CYAN}Please specify a branch.${RESET}" + return 1 + fi + + # Confirm action, as local changes will be lost + echo -e "${BRIGHT_BLUE}Selected branch to reset is: ${BRIGHT_YELLOW}${BRANCH}${RESET}" + if ask "${BRIGHT_RED}WARNING: ${BRIGHT_CYAN}If you have any local changes, they will be lost! ${BRIGHT_MAGENTA}Are you sure?${RESET}" 'N'; then + git checkout "${BRANCH}" && git fetch --all && git reset --hard "origin/${BRANCH}" + else + echo -e "${BRIGHT_RED}Operation cancelled${RESET}" + return 0 + fi + } + + # Retrieve a specific version of a file from Git history + function gitfileversion() { + # Constants + local NO_ARGS=0 + local NO_GIT_REPO_ERROR=1 + local NO_FILE_PATH_ERROR=2 + local FILE_NOT_IN_GIT_ERROR=3 + local NO_SELECTION_MADE=4 + + # Display help + function display_help() { + echo -e "${BRIGHT_BLUE}Usage:${RESET} git_file_version ${BRIGHT_GREEN}${RESET} [${BRIGHT_YELLOW}output file${RESET}]" + echo -e "${BRIGHT_BLUE}Description:${RESET} Retrieves a specific version of a file from Git history" + echo -e " ${BRIGHT_GREEN}${RESET} - Path of the file in the Git repository" + echo -e " ${BRIGHT_YELLOW}${RESET} - Optional. Path to save the older version of the file. If not provided, copies to clipboard" + } + + # Check for no arguments + if [[ $# -eq ${NO_ARGS} ]]; then + display_help + return ${NO_ARGS} + fi + + # Check if inside a Git repository + if ! git rev-parse --is-inside-work-tree > /dev/null 2>&1; then + echo -e "${BRIGHT_RED}ERROR: ${BRIGHT_CYAN}Not inside a Git repository${RESET}" + return ${NO_GIT_REPO_ERROR} + fi + + # Check for missing file path + if [[ -z "$1" ]]; then + echo -e "${BRIGHT_RED}ERROR: ${BRIGHT_CYAN}File path not provided${RESET}" + display_help + return ${NO_FILE_PATH_ERROR} + fi + + # Check if the file exists in the Git repo + if ! git ls-files --error-unmatch "$1" > /dev/null 2>&1; then + echo -e "${BRIGHT_RED}ERROR: ${BRIGHT_CYAN}File '${BRIGHT_YELLOW}$1${BRIGHT_CYAN}' not found in Git repository${RESET}" + return ${FILE_NOT_IN_GIT_ERROR} + fi + + # Display the commit menu and get selection + local COMMIT_HASH + COMMIT_HASH=$(git log --pretty=format:"%h - %s (%cr)" -- "$1" | createmenu) + if [[ -z "${COMMIT_HASH}" ]]; then + echo -e "${BRIGHT_RED}No selection made. Exiting.${RESET}" + return ${NO_SELECTION_MADE} + fi + + # Extract the hash from the selection + COMMIT_HASH=${COMMIT_HASH%% *} + + # Retrieve the file for the selected commit + if [[ -n "$2" ]]; then + # Save to file if second parameter is provided + git show "${COMMIT_HASH}:$1" > "$2" + else + git show "${COMMIT_HASH}:$1" | clipboard + fi + } + + # Helper to create git branch names + gitbranchhelp() { + local BRANCH_PREFIXES=( + "bugfix/" + "develop/" + "feature/" + "hotfix/" + "master/" + "main/" + "release/" + "support/" + "wip/" + ) + + local BRANCH_DESCRIPTIONS=( + "Non-critical bug fixes." + "Ongoing development work." + "New features or enhancements." + "Critical production bug fixes." + "Stable, production-ready code." + "Stable, production-ready code." + "Pre-release preparations." + "Support for older releases." + "Work in progress." + ) + + # Prompt the user to select the type of branch from the list + echo -e "${BRIGHT_YELLOW}Select the type of branch:${RESET}" + for i in "${!BRANCH_PREFIXES[@]}"; do + echo -e "$((i+1)). ${BRIGHT_CYAN}${BRANCH_PREFIXES[${i}]}${RESET} - ${BRANCH_DESCRIPTIONS[${i}]}" + done + + # Read the user's selection for the branch type + local BRANCH_TYPE_NUMBER + echo -ne "${BRIGHT_YELLOW}Enter the number corresponding to the branch type:${RESET} " + read -r BRANCH_TYPE_NUMBER + + # Validate the selection + if ! [[ "${BRANCH_TYPE_NUMBER}" =~ ^[1-9]$ ]]; then + echo -e "${BRIGHT_RED}ERROR: ${BRIGHT_CYAN}Invalid selection${RESET}" + return 1 + fi + + local BRANCH_PREFIX="${BRANCH_PREFIXES[$((BRANCH_TYPE_NUMBER-1))]}" + + # Get the project code + local PROJECT_CODE + echo -ne "${BRIGHT_YELLOW}Enter the project code ${BRIGHT_CYAN}(e.g., PROJ)${BRIGHT_YELLOW} or press return to skip${RESET}: " + read -r PROJECT_CODE + + # Get the ticket number + local TICKET_NUMBER + echo -ne "${BRIGHT_YELLOW}Enter the ticket number ${BRIGHT_CYAN}(e.g., 12345)${BRIGHT_YELLOW} or press return to skip${RESET}: " + read -r TICKET_NUMBER + + # Get the description + local DESCRIPTION + echo -ne "${BRIGHT_YELLOW}Enter the description ${BRIGHT_CYAN}(use spaces or dashes, will be formatted automatically)${RESET}: " + read -r DESCRIPTION + + # Format the description: replace spaces with dashes, remove duplicate dashes + DESCRIPTION=$(echo "${DESCRIPTION}" | tr '[:space:]' '-' | tr -s '-') + + # Construct the branch name parts + local BRANCH_NAME="${BRANCH_PREFIX}" + + # Append project code if provided + if [[ -n "${PROJECT_CODE}" ]]; then + BRANCH_NAME+="${PROJECT_CODE}" + fi + + # Append ticket number with a dash if project code is present or ticket number is provided + if [[ -n "${TICKET_NUMBER}" ]]; then + if [[ -n "${PROJECT_CODE}" ]]; then + BRANCH_NAME+="-${TICKET_NUMBER}" + else + BRANCH_NAME+="${TICKET_NUMBER}" + fi + fi + + # Append description with a dash if previous parts are present + if [[ -n "${DESCRIPTION}" ]]; then + if [[ -n "${PROJECT_CODE}" || -n "${TICKET_NUMBER}" ]]; then + BRANCH_NAME+="-${DESCRIPTION,,}" + else + BRANCH_NAME+="${DESCRIPTION,,}" + fi + fi + + # Remove any trailing dashes + BRANCH_NAME=$(echo "${BRANCH_NAME}" | sed 's/-$//') + + # Copy the commit message to the clipboard + echo "${BRANCH_NAME}" | clipboard + + # Display the final branch name + echo -e "${BRIGHT_GREEN}Your branch name is:${RESET}\n${BRANCH_NAME}" + } + + # Helper to create git commit messages + gitcommithelp() { + local FULL_COMMIT_MESSAGE + + # Define commit types with colored labels + local COMMIT_TYPES=( + "🔧 ${BRIGHT_CYAN}chore${RESET}: updating grunt tasks etc; no production code change" + "📝 ${BRIGHT_CYAN}docs${RESET}: changes to the documentation" + "✨ ${BRIGHT_CYAN}feat${RESET}: new feature for the user, not a new feature for build script" + "🐛 ${BRIGHT_CYAN}fix${RESET}: bug fix for the user, not a fix to a build script" + "♻️ ${BRIGHT_CYAN}refactor${RESET}: refactoring production code, eg. renaming a variable" + "💅 ${BRIGHT_CYAN}style${RESET}: formatting, missing semi colons, etc; no production code change" + "🧪 ${BRIGHT_CYAN}test${RESET}: adding missing tests, refactoring tests; no production code change" + "🚧 ${BRIGHT_CYAN}wip${RESET}: work in progress" + "🔀 ${BRIGHT_CYAN}merge${RESET}: choose this if this is a merge" + ) + + # Corresponding commit prefixes + local COMMIT_PREFIXES=( + "🔧 chore: " + "📝 docs: " + "✨ feat: " + "🐛 fix: " + "♻️ refactor: " + "💅 style: " + "🧪 test: " + "🚧 wip: " + ) + + # Prompt the user to select the type of commit from the list + echo -e "${BRIGHT_YELLOW}What type of commit is this?${RESET}" + for i in "${!COMMIT_TYPES[@]}"; do + echo -e "$((i+1)). ${COMMIT_TYPES[${i}]}" + done + + # Read the user's selection + local COMMIT_TYPE_NUMBER + echo -ne "${BRIGHT_YELLOW}Enter the number corresponding to the commit type${RESET}: " + read -r COMMIT_TYPE_NUMBER + + # Validate the selection + if ! [[ "${COMMIT_TYPE_NUMBER}" =~ ^[1-9]$ ]]; then + echo -e "${BRIGHT_RED}ERROR: ${BRIGHT_CYAN}Invalid selection${RESET}" + return 1 + fi + + # Handle special cases (merge and wip) + if [[ "${COMMIT_TYPE_NUMBER}" -eq 9 ]]; then + # Merge commit + local SOURCE_BRANCH + echo -ne "${BRIGHT_YELLOW}Enter the ${BRIGHT_CYAN}source branch${BRIGHT_YELLOW} you are merging ${BRIGHT_CYAN}from${BRIGHT_YELLOW}${RESET}: " + read -r SOURCE_BRANCH + + # Ensure the branch was provided + if [[ -z "${SOURCE_BRANCH}" ]]; then + echo -e "${BRIGHT_RED}ERROR: ${BRIGHT_CYAN}No source branch specified${RESET}" + return 1 + fi + + local DESTINATION_BRANCH + echo -ne "${BRIGHT_YELLOW}Enter the ${BRIGHT_CYAN}destination branch${BRIGHT_YELLOW} you are merging ${BRIGHT_CYAN}into${BRIGHT_YELLOW}${RESET}: " + read -r DESTINATION_BRANCH + + # Ensure the branch was provided + if [[ -z "${DESTINATION_BRANCH}" ]]; then + echo -e "${BRIGHT_RED}ERROR: ${BRIGHT_CYAN}No destination branch specified${RESET}" + return 1 + fi + + # Construct the merge commit message + FULL_COMMIT_MESSAGE="🔀 Merged branch ${SOURCE_BRANCH} into ${DESTINATION_BRANCH}" + + elif [[ "${COMMIT_TYPE_NUMBER}" -eq 8 ]]; then + # WIP commit + FULL_COMMIT_MESSAGE="🚧 work in progress [$(date +%H:%M)]" + + else + # Regular commit + # Get the corresponding commit prefix + local COMMIT_PREFIX="${COMMIT_PREFIXES[$((COMMIT_TYPE_NUMBER-1))]}" + + # Prompt the user for a commit message + local COMMIT_MESSAGE + echo -e "${BRIGHT_YELLOW}Enter a commit message ${BRIGHT_MAGENTA}(use present tense and do not capitalize the first letter)${RESET}:" + echo -ne "${BRIGHT_GREEN}>${RESET} " + read -r COMMIT_MESSAGE + + # Ensure a commit message was provided + if [[ -z "${COMMIT_MESSAGE}" ]]; then + echo -e "${BRIGHT_RED}ERROR: ${BRIGHT_CYAN}No commit message was specified${RESET}" + return 1 + fi + + # Combine the prefix and message + FULL_COMMIT_MESSAGE="${COMMIT_PREFIX}${COMMIT_MESSAGE}" + fi + + # Copy the commit message to the clipboard + echo "${FULL_COMMIT_MESSAGE}" | clipboard + + # Display the final commit message + echo -e "${BRIGHT_GREEN}Your commit message is:${RESET}\n${FULL_COMMIT_MESSAGE}" + } + + # Set defaults for Git (~/.gitconfig) + function gitfixsettings() { + local CHECKMARK="\r${BRIGHT_GREEN}✓${RESET}" + + # If GitAlias (see gitalias alias) is installed... + if [[ -f "${HOME}/gitalias.txt" ]]; then + # Tell Git to include this file + git config --global include.path "gitalias.txt" + echo -e "${CHECKMARK} Found gitalias.txt" + fi + + # If a global ignore file exists... + if [[ -f "${HOME}/.gitignore_global" ]]; then + # Globally exclude common files that you don’t want to track + git config --global core.excludesfile ~/.gitignore_global + echo -e "${CHECKMARK} Found .gitignore_global" + fi + + # Set the user name + local GIT_USER_NAME + read -r -e -i "$(git config --get user.name)" -p "Enter your user name (ENTER to skip): " GIT_USER_NAME + if [ -z "${GIT_USER_NAME}" ]; then + GIT_USER_NAME="User" + fi + git config --global user.name "${GIT_USER_NAME}" + + # Set the email address + local GIT_USER_EMAIL + read -r -e -i "$(git config --get user.email)" -p "Enter your email (ENTER to skip): " GIT_USER_EMAIL + if [ -z "${GIT_USER_EMAIL}" ]; then + GIT_USER_EMAIL="user@example.com" + fi + git config --global user.email "${GIT_USER_EMAIL}" + + # You should add this manually with your own information + # git config --global user.signingkey + + # Set the default branch name to be main (instead of master) in new repos + # git config --global init.defaultBranch main + + # Enable colorful output for better readability + git config --global color.ui auto + + # Check integrity of objects during operations like fetching or receiving + git config --global transfer.fsckobjects true + git config --global fetch.fsckobjects true + git config --global receive.fsckobjects true + + # Automatically fix minor mistakes when you mistype a command + # 0 - No autocorrection; only provides suggestions + # 30 - Executes after 3 seconds + # immediate - Executes immediately without confirmation + # prompt - Asks for confirmation before executing the suggestion + git config --global help.autocorrect prompt + + # Automatically set up a tracking branch (upstream branch) when you push + # a local branch to a remote for the first time, so you will not have + # to manually run "git push --set-upstream origin " each time + git config --global push.autoSetupRemote true + + # Speed up git status in large repositories by using Git fsmonitor + # Note: This can cause issues on much older out of date Git versions + git config --global core.fsmonitor true + + # Set the default Git editor to your default Bash editor + git config --global core.editor "${EDITOR}" + echo -e "${CHECKMARK} Default editor set to ${EDITOR}" + + # Set the default diff algorithm + # The patience diff algorithm handles reordering changes better, + # it can be slower for larger diffs compared to Myers + # The Histogram algorithm performs better with large codebases + # but may produce slightly less intuitive results + git config --global diff.algorithm patience + # git config --global diff.algorithm histogram + + # Set the dafault pager + git config --global core.pager "less --ignore-case --LONG-PROMPT --LINE-NUMBERS" + + # Set defaults for diff and merge + git config --global merge.conflictstyle "diff3" + git config --global diff.colorMoved "default" + + # Set this if icdiff is installed + if hascommand --strict icdiff; then + git config --global icdiff.options '--highlight --line-numbers --strip-trailing-cr' + echo -e "${CHECKMARK} Found icdiff" + fi + + # If delta is installed... + if hascommand delta; then + + # delta - Beautiful side by side colored diff with Git support and syntax highlighting + # This diff tool also uses more advanced diff algorithms with more features + # Link: https://github.com/dandavison/delta + # Info: Add listed settings to your ~/.gitconfig + # NOTE: use n and N to move between diff sections + git config --global core.pager "delta" + + # Enable delta for interactive diffs + git config --global interactive.diffFilter "delta --color-only --features=interactive" + + # Enable navigation using 'n' and 'N' for diff sections + git config --global delta.navigate true + + # Enable delta features: side-by-side diff view, line numbers, decorations, and word-diff + git config --global delta.features "side-by-side line-numbers decorations word-diff" + + # Side-by-side diff format + git config --global delta.side-by-side "true" + + # Display line numbers in diffs + git config --global delta.line-numbers "true" + + # Use word-diff and define word boundaries with regex + git config --global delta.word-diff-regex '\w+' + + # Set how delta handles whitespace errors (underline them in bold red) + git config --global delta.whitespace-error-style "underline bold red" + + # Limit the length of lines to highlight whitespace issues + git config --global delta.whitespace-line-length-limit 120 + + # Customize commit decoration (blue outline, raw commit message style) + git config --global delta.decorations.commit-decoration-style "blue ol" + git config --global delta.decorations.commit-style "raw" + + # Customize hunk header (blue box for decoration, red file name, green line numbers) + git config --global delta.decorations.hunk-header-decoration-style "blue box" + git config --global delta.decorations.hunk-header-file-style "red" + git config --global delta.decorations.hunk-header-line-number-style "#067a00" + git config --global delta.decorations.hunk-header-style "file line-number syntax" + + # Disable plus/minus markers in interactive mode + git config --global delta.interactive.keep-plus-minus-markers "false" + + # Omit file names in the delta output + git config --global delta.decorations.file-style "omit" + + # (Optional) Disable showing GPG signature in `git log` (commented out) + # git config --global log.showSignature false + + echo -e "${CHECKMARK} Found delta" + + # Check for Difftastic + elif hascommand difft; then + + # Difftastic supports over 30 languages and compares based on syntax + # git -c diff.external=difft diff # View uncommitted changes + # git -c diff.external=difft show --ext-diff # Most recent commit + # git -c diff.external=difft log -p --ext-diff # Recent commits current branch + # Link: https://github.com/Wilfred/difftastic + # Link: https://difftastic.wilfred.me.uk/git.html?highlight=page#difftool + git config --global diff.external difft + + # Define difftastic as a custom difftool with the necessary command + git config --global difftool.difftastic.cmd 'difft "$MERGED" "$LOCAL" "abcdef1" "100644" "$REMOTE" "abcdef2" "100644"' + + # Additional configurations for difftastic + git config --global difftool.prompt false + git config --global pager.difftool true + git config --global diff.tool difftastic + + # Set up aliases "git dlog", "git dshow", and "git ddiff" + git config --global alias.dlog '-c diff.external=difft log -p --ext-diff' + git config --global alias.dshow '-c diff.external=difft show --ext-diff' + git config --global alias.ddiff '-c diff.external=difft diff' + + echo -e "${CHECKMARK} Found Difftastic" + + # Check for diff-so-fancy + elif hascommand diff-so-fancy; then + + # Configure diff-so-fancy for Git diff outputs + # diff-so-fancy simplifies Git diff outputs, making them easier to read + git config --global core.pager "diff-so-fancy | $(command which less) --tabs=4 -RFX" + + # Interactive diff filter setting for diff-so-fancy + git config --global interactive.diffFilter "diff-so-fancy --patch" + + # Setting color enhancements for better visual differentiation in diffs + git config --global color.ui true + git config --global color.diff-highlight.oldNormal "red bold" + git config --global color.diff-highlight.oldHighlight "red bold 52" + git config --global color.diff-highlight.newNormal "green bold" + git config --global color.diff-highlight.newHighlight "green bold 22" + git config --global color.diff.meta "11" + git config --global color.diff.frag "magenta bold" + git config --global color.diff.commit "yellow bold" + git config --global color.diff.old "red bold" + git config --global color.diff.new "green bold" + git config --global color.diff.whitespace "red reverse" + + # Confirm diff-so-fancy is configured for output + echo -e "${CHECKMARK} Configured diff-so-fancy for Git diffs" + fi + + # If we are in a desktop environment and Ultracompare is not installed... + # You will be prompted when you launch Ultracompare to automatically set up + # Git, so just skip the other diff configurations since this app is paid + # Link: https://www.ultraedit.com/products/ultracompare-linux/ + if ([[ -n "$DISPLAY" ]] || [[ -n "$WAYLAND_DISPLAY" ]]) && [[ -n "$XDG_CURRENT_DESKTOP" ]]; then + echo -e "${CHECKMARK} User is in desktop environment ${XDG_CURRENT_DESKTOP}" + + # If UltraCompare is installed... + # You will be prompted when you launch UltraCompare to automatically set up + # Git, so just skip the other diff configurations since this app is paid + # Link: https://www.ultraedit.com/products/ultracompare-linux/ + if hascommand --strict ucx; then + echo -e "${CHECKMARK} Found UltraCompare" + + # If Meld is installed... + # Link: https://meldmerge.org/ + elif hascommand --strict meld; then + # These settings will not alter the behaviour of "git diff" + # Link: https://stackoverflow.com/questions/34119866/setting-up-and-using-meld-as-your-git-difftool-and-mergetool + # You use "git difftool" in exactly the same way as you use git diff + # Example: git difftool file_name + # Example: git difftool file_name + # Example: git difftool file_name + git config --global diff.tool meld + git config --global difftool.meld.path "$(command which meld)" + git config --global difftool.meld.cmd 'meld "$LOCAL" "$REMOTE"' + git config --global difftool.prompt false + + # "git mergetool" allows you to use a GUI merge program to resolve merge conflicts + # Before using git mergetool you perform a merge in the usual way with git + # then Mergetool can now be used to resolve the merge conflicts + git config --global merge.tool meld + git config --global mergetool.meld.path "$(command which meld)" + git config --global mergetool.prompt false + + # Choose which starting edit position you'd prefer; $MERGED for the file + # which contains the partially merged file with the merge conflict info + # or $BASE for the shared commit ancestor of $LOCAL and $REMOTE + git config --global mergetool.meld.cmd 'meld "$LOCAL" "$MERGED" "$REMOTE" --output "$MERGED"' + #git config --global mergetool.meld.cmd 'meld "$LOCAL" "$BASE" "$REMOTE" --output "$MERGED"' + + # Also set up the alias "git meld" which works on Cygwin + git config --global alias.meld '!git difftool -t meld --dir-diff' + echo -e "${CHECKMARK} Found meld" + + # If KDiff3 is installed... + # Link: https://kdiff3.sourceforge.net/ + elif hascommand --strict kdiff3; then + git config --global diff.guitool kdiff3 + git config --global difftool.kdiff3.path "$(command which kdiff3)" + git config --global difftool.kdiff3.trustExitCode false + git config --global merge.tool kdiff3 + git config --global mergetool.kdiff3.path "$(command which kdiff3)" + git config --global mergetool.kdiff3.trustExitCode false + echo -e "${CHECKMARK} Found KDiff3" + fi + fi + + # Create an alias s for the git status command + git config --global alias.s "status" + + # Create an alias l for a compact log format: + # --oneline : shows each commit as a single line + # --graph : adds an ASCII graph of branch and merge history + # --decorate : adds extra information such as branch names, tags, etc. + git config --global alias.l "log --oneline --date=short --graph --decorate --pretty=format:'%C(red)%h%C(reset) - %C(green)(%cr)%C(reset) %s%C(yellow)%d%C(reset) %C(bold blue)<%an>%C(reset)'" + + # Stage git commits with an interactive prompt + git config --global alias.aa "add -p" + + # Make an empty commit with no content + # (to trigger kick off a CI build or some other integration) + git config --global alias.empty "commit --allow-empty -m" + } +fi + +####################################################### +### SYSTEMD ALIASES +####################################################### + +# Check if systemd is installed +if hascommand --strict systemctl; then + # Get a list of all services + alias services='systemctl list-units --type=service --state=running,failed' + alias servicesall='systemctl list-units --type=service' + + # Find what systemd services have failed + alias {failed,servicefailed}='systemctl --failed' + + # Get the status of a services + alias servicestatus='sudo systemctl status' + + # Start a service and enable automatic startup at boot + alias serviceenable='sudo systemctl enable --now' + + # Disable a service + alias servicedisable='sudo systemctl disable' + + # Start a service + alias servicestart='sudo systemctl start' + + # Stop a service + alias servicestop='sudo systemctl stop' + + # Forcefully terminate a service + alias servicekill='sudo systemctl kill' + + # Stop and restart a service + alias servicerestart='sudo systemctl restart' + + # Reload a service's configuration (soft restart) + alias servicereload='sudo systemctl reload' + + # Tell how long it took to boot the system + alias boottime='systemd-analyze' + + # Tell how long it took to boot the system + alias boottimelist='systemd-analyze blame' + + # Generate a visual boot time analysis chart as a graphic SVG image + alias bootchart='systemd-analyze plot > ./boot.svg && echo "Boot chart saved to ${PWD/#$HOME/~}/boot.svg"' + + # View kernel messages from current boot + alias bootmessages='journalctl -b -k' + + # View kernel messages from previous boot + alias bootprevious='journalctl -b -1 -k' + + # Filter for warning and errors + alias booterrors='journalctl -b -k -p warning..emerg' + + # List boots + alias bootlist='journalctl --list-boots' + + # Clear system log entries from the systemd journal + alias {clearsystemlogs,cleansystemlogs}='echo -ne "${BRIGHT_BLUE}Before${RESET}: "; journalctl --disk-usage; sudo journalctl --rotate; sudo journalctl --vacuum-time=1s; echo -ne "${BRIGHT_BLUE}After${RESET}: "; journalctl --disk-usage' + + # Fast reboot user space and not the kernel + alias rebootfast='systemctl soft-reboot' + + # If SSH is installed... + if hascommand --strict ssh; then + # Create aliases to start/enable and stop/disable the SSH server + alias sshstatus='systemctl status sshd.service' + alias sshstart='sudo systemctl start sshd.service && sudo systemctl enable sshd.service' + alias sshstop='sudo systemctl stop sshd.service && sudo systemctl disable sshd.service' + alias sshrestart='sudo systemctl restart sshd.service && sudo systemctl enable sshd.service' + fi + + # If gpm is installed... + # Link: https://github.com/telmich/gpm + if hascommand --strict gpm; then + alias ttymouseon='sudo systemctl enable --now gpm' + alias ttymouseoff='sudo systemctl stop gpm && sudo systemctl disable gpm' + alias ttymousestatus='sudo systemctl status gpm' + fi + + # Flushing and restart the DNS cache if installed and running + function flushdns() { + # Check if systemd-resolved is available + if hascommand --strict systemd-resolve; then + # Check if systemd-resolved service is active + if systemctl is-active systemd-resolved >/dev/null; then + # Flush DNS cache and restart systemd-resolved + sudo systemd-resolve --flush-caches + sudo systemctl restart systemd-resolved + echo "DNS cache flushed and systemd-resolved restarted" + else + echo "systemd-resolved is not active, unable to flush DNS cache" + fi + # Check if dnsmasq is available + elif hascommand --strict dnsmasq; then + # Check if dnsmasq process is running + if pgrep -x "dnsmasq" >/dev/null; then + # Restart dnsmasq to flush DNS cache + sudo systemctl restart dnsmasq + echo "DNS cache flushed and dnsmasq restarted" + else + echo "dnsmasq is not running, unable to flush DNS cache" + fi + else + echo "No supported DNS caching service found" + fi + } + + # Browse and manage systemd services + findservice() { + local service + local services_list + + # Get only the service names (first column, stripping status symbols) + services_list=$(systemctl list-units --type=service --all --no-legend --plain | awk '{print $1}' | sed 's/^●//') + + # Pickers with preview support + local PICKER_FOUND=false + for picker in sk fzf; do + if hascommand "$picker"; then + PICKER_FOUND=true + service=$(echo "$services_list" | "$picker" --header="Select service" \ + --preview 'systemctl status {}' | xargs) + break + fi + done + + # Fallback to pickers without preview + if ! ${PICKER_FOUND}; then + for picker in fzy peco percol pick icepick selecta sentaku zf; do + if hascommand "$picker"; then + PICKER_FOUND=true + service=$(echo "$services_list" | "$picker" | xargs) + break + fi + done + fi + + # No picker found + if ! ${PICKER_FOUND}; then + echo "Error: No fuzzy finder installed (fzf, sk, fzy, peco, etc.)" + return 1 + fi + + # User cancelled the picker + if [ -z "$service" ]; then + return 0 + fi + + echo "Service: $service" + echo "1) Status 2) Start 3) Stop 4) Restart 5) Enable 6) Disable" + read -p "Action: " -n 1 action + echo + case $action in + 1) systemctl status "$service" ;; + 2) sudo systemctl start "$service" ;; + 3) sudo systemctl stop "$service" ;; + 4) sudo systemctl restart "$service" ;; + 5) sudo systemctl enable "$service" ;; + 6) sudo systemctl disable "$service" ;; + esac + } +fi + +####################################################### +### CHANGING AND LISTING DIRECTORIES +####################################################### + +# Aliases for faster pushd and popd: type "p" for pushd and "p-" for popd +# Link: https://opensource.com/article/19/8/navigating-bash-shell-pushd-popd +alias p='pushd' +alias p-='popd' +# Allow us to clear the directory stack +alias dirsclear='dirs -c' +# If you want to see the numeric position of each directory in the stack, +# you can use the -v (vertical) option - as DT suggests, use this by default +alias dirs='dirs -v' + +# If Ranger is installed, maintain the last selected directory... +# Link: https://github.com/ranger/ranger +# g - navigation and tabs +# r - :open_with command +# y - copy (yank) +# d - cut (delete) +# p - paste +# o - sort +# . - filter_stack +# z - settings / zz - filter +# u - undo +# M - linemode +# +, -, = for setting access rights to files +if hascommand --strict ranger; then + # Override ranger to change to the most recently selected directory + function ranger() { + # If the temporary directory location is set... + if [[ -n "${TEMPDIR_LOCAL}" ]] && [[ -d "${TEMPDIR_LOCAL}" ]]; then + # Use system ranger and save last directory on exit + command ranger --choosedir="${TEMPDIR_LOCAL}/.rangerdir" "$@" + + # If last directory file exists, change to it + if [[ -f "${TEMPDIR_LOCAL}/.rangerdir" ]]; then + local LAST_DIR_LOCATION="$(< "${TEMPDIR_LOCAL}/.rangerdir")" + if [[ -d "${LAST_DIR_LOCATION}" ]]; then + command cd "${LAST_DIR_LOCATION}" || { + echo -e "${BRIGHT_RED}Error:${BRIGHT_CYAN} Unable to change directory to '${LAST_DIR_LOCATION}'${RESET}" + return 1 + } + command rm -f "${TEMPDIR_LOCAL}/.rangerdir" > /dev/null 2>&1 + fi + fi + else + # Launch without directory persistence if TEMPDIR_LOCAL is not set + command ranger "$@" + fi + } + + # Set a quick shortcut key to launch ranger + alias r='ranger' +fi + +# If nnn is installed, add an alias for a directory listing +# Link: https://github.com/jarun/nnn/ +# The following command installs or updates all plugins: +# Install: curl -Ls https://raw.githubusercontent.com/jarun/nnn/master/plugins/getplugs | sh +# Plugins are installed to ${XDG_CONFIG_HOME:-$HOME/.config}/nnn/plugins +if hascommand --strict nnn; then + if [[ -f "${XDG_CONFIG_HOME:-${HOME}/.config}/nnn/plugins/finder" ]]; then + export NNN_PLUG='f:finder;o:fzopen;p:mocplay;d:diffs;t:nmount;v:imgview' + alias nnnplugins='curl -Ls https://raw.githubusercontent.com/jarun/nnn/master/plugins/getplugs | sh' + fi + #alias nnn='nnn -e' + alias nnn='nnn -Headr' +fi + +# TUIFI Manager +# Link: https://github.com/GiorgosXou/TUIFIManager +# Install: pip3 install tuifimanager --upgrade +if hascommand tuifi; then + alias ff="tuifi" +fi + +# If lsx enhanced directory navigation is found, source it +# Link: https://github.com/souvikinator/lsx +if [[ -f "${XDG_CONFIG_HOME:-${HOME}/.config}/lsx/lsx.sh" ]]; then + builtin source "${XDG_CONFIG_HOME:-${HOME}/.config}/lsx/lsx.sh" + + # If lsx is installed, alias it to see hidden directories + if hascommand lsx; then + alias l='lsx -a' + elif hascommand ls-x; then + alias l="ls-x -a" + alias lsx="${GOPATH}/bin/ls-x" + elif [[ -x "${GOPATH}/bin/ls-x" ]]; then + alias l="${GOPATH}/bin/ls-x -a" + alias lsx="${GOPATH}/bin/ls-x" + fi +fi + +# List folders recursively in a tree +if hascommand --strict tree; then + alias treed='command tree -CAFd' +fi + +# List files recursively in a tree +if hascommand broot; then + # If broot is installed... + # Link: https://github.com/Canop/broot + # Link: https://computingforgeeks.com/broot-easy-directory-trees-navigation-in-linux/ + + # To generate this source file, type: broot --install + if [[ -f "${XDG_CONFIG_HOME:-${HOME}/.config}/broot/launcher/bash/br" ]]; then + # Source the br function + builtin source "${XDG_CONFIG_HOME:-${HOME}/.config}/broot/launcher/bash/br" + + # If Git is installed, enable the Git functionality + if hascommand --strict git; then + alias {lb,tree}='br --sizes --dates --show-root-fs --hidden --show-git-info' + else + alias {lb,tree}='br --sizes --dates --show-root-fs --hidden' + fi + else + # If Git is installed, enable the Git functionality + if hascommand --strict git; then + alias {lb,tree}='broot --sizes --dates --show-root-fs --hidden --show-git-info' + else + alias {lb,tree}='broot --sizes --dates --show-root-fs --hidden' + fi + fi + +elif [[ $_SKIP_LSD = false ]] && hascommand lsd; then + # If lsd is installed... + # Link: https://github.com/Peltoche/lsd + alias tree='command lsd --all --blocks permission,user,size,date,name --group-dirs first --header --long --tree' +elif [[ $_SKIP_EXA = false ]] && hascommand exa; then + # If exa is installed... + # Link: https://github.com/ogham/exa + if hascommand --strict git; then + alias tree='exa --all --git --group-directories-first --header --long --tree' + else + alias tree='exa --all --group-directories-first --header --long --tree' + fi +elif hascommand tree; then + # If tree is installed... + # Link: https://www.tecmint.com/linux-tree-command-examples/ + alias tree='command tree -CAhF --dirsfirst' +fi + +# Common aliases for directory listing commands +alias lw='ls -xAh' # wide listing format +alias lm='ll -alh | command less -S' # pipe through less +alias lr='ls -lRh' # recursive ls +alias l.='ll -d .*' # show hidden files +alias lfile="ls -l | grep -v '^d'" # files only +alias ldir="ls -la | grep '^d'" # directories only + +# Conditional aliases based on the availability of specific directory tools +if [[ $_SKIP_LS_COLORIZED = false ]]; then + + # If eza (with Git support) is installed... + # Eza is the fastest ls replacements with so many features + # Link: https://eza.rocks + if [[ $_SKIP_EXA = false ]] && hascommand eza; then + + # Flags for nearly all directory listings + _EZA_COMMON_FLAGS="--all --classify --color=auto --color-scale=all --icons=auto" + + # Flags for all long listings (which include common and Git flags) + #_EZA_LONG_FLAGS="${_EZA_COMMON_FLAGS} --group-directories-first --long --links --group --modified" + _EZA_LONG_FLAGS="${_EZA_COMMON_FLAGS} --group-directories-first --long --links --group --modified" + + alias ls="eza ${_EZA_COMMON_FLAGS} --group-directories-first" + alias labc="eza ${_EZA_COMMON_FLAGS} --grid --sort name" + alias ll="eza ${_EZA_LONG_FLAGS}" + alias lx="eza ${_EZA_LONG_FLAGS} --sort extension" + alias lk="eza ${_EZA_LONG_FLAGS} --sort size" + alias lt="eza ${_EZA_LONG_FLAGS} --sort modified" + alias lc="eza ${_EZA_LONG_FLAGS} --changed --sort changed" + alias lu="eza ${_EZA_LONG_FLAGS} --accessed --sort accessed" + alias new="eza ${_EZA_LONG_FLAGS} --sort modified | tail -10 | tac" + alias lr="eza ${_EZA_COMMON_FLAGS} --grid --group-directories-first --recurse" + alias ltree="command eza --icons=auto --all --group-directories-first --header --long --tree" + + # Cleanup variables to avoid leaking them into the environment + unset _EZA_COMMON_FLAGS _EZA_LONG_FLAGS + + # If lsd is installed, use it instead + # Link: https://github.com/Peltoche/lsd + elif [[ $_SKIP_LSD = false ]] && hascommand lsd; then + alias ls='lsd -AF --group-dirs first' + alias ll='lsd --almost-all --header --long --group-dirs first' + alias labc='lsd -lAv --header' + alias lx='lsd -lAXh --header' + alias lk='lsd -lASrh --header' + alias lt='lsd -lAtrh --header' + alias lc='command ls -lAcrh --color=always' + alias lu='command ls -lAurh --color=always' + alias new='lsd -lAtr --almost-all | tail -10 | tac' + alias ltree='command lsd --almost-all --blocks permission,user,size,date,name --group-dirs first --header --long --tree' + + # If exa (depreciated but still on some older distros) is installed... + # Link: https://github.com/ogham/exa + elif [[ $_SKIP_EXA = false ]] && hascommand exa; then + + # Add icons to exa if unicode and the icon paramter is supported + if [[ $(locale charmap) == 'UTF-8' ]]; then + if command exa --help | grep -q -- "--icons" &> /dev/null; then + aliasappend 'exa' '--icons' + fi + fi + + # Flags for nearly all directory listings + _EXA_COMMON_FLAGS="--all --classify --color=auto --color-scale" + + # Flags for all long listings (which include common and Git flags) + _EXA_LONG_FLAGS="${_EXA_COMMON_FLAGS} --group-directories-first --long --links --group --modified" + + alias ls="exa ${_EXA_COMMON_FLAGS} --group-directories-first" + alias labc="exa ${_EXA_COMMON_FLAGS} --grid --sort name" + alias ll="exa ${_EXA_LONG_FLAGS}" + alias lx="exa ${_EXA_LONG_FLAGS} --sort extension" + alias lk="exa ${_EXA_LONG_FLAGS} --sort size" + alias lt="exa ${_EXA_LONG_FLAGS} --sort modified" + alias lc="exa ${_EXA_LONG_FLAGS} --changed --sort changed" + alias lu="exa ${_EXA_LONG_FLAGS} --accessed --sort accessed" + alias new="exa ${_EXA_LONG_FLAGS} --sort modified | tail -10 | tac" + alias lr="exa ${_EXA_COMMON_FLAGS} --grid --group-directories-first --recurse" + alias ltree="command exa --all --group-directories-first --header --long --tree" + + # Clean up variables to avoid leaking them into the environment + unset _EXA_COMMON_FLAGS _EXA_LONG_FLAGS + + # If Natls is installed, use it instead + # Link: https://github.com/willdoescode/nat + # Install: cargo install natls + elif hascommand natls; then + alias ls='natls --gdf --name' + alias ll='natls --gdf --name --long' + alias labc='natls --name' + alias lx='command ls -FlsaXBh --color=auto' + alias lk='natls --gdf --long --size' + alias lt='natls --gdf --long --modified' + alias lc='command ls -Flsacrh --color=auto' + alias lu='command ls -Flsaurh --color=auto' + alias new='command ls -latr | tail -10 | tac' + alias l.='command ls -Flsd .* --color=auto' + alias lr='command ls -lRh --color=auto' + else + # Use ls with command line options + alias ls='ls -aFh --color=always --group-directories-first' + alias labc='ls -lap' # sort alphabetically + alias lx='ll -laXBh' # sort by extension + alias lk='ll -laSrh' # sort by size + alias lt='ll -latrh' # sort by date + alias lc='ll -lacrh' # sort by change time + alias lu='ll -laurh' # sort by access time + alias new='ls -latr | tail -10 | tac' # list recently created/updated files + alias ltree='command tree -CAhF --dirsfirst' # tree view + + if [[ $_SKIP_GRC = false ]] && hascommand grc; then + # If grc Generic Colouriser is installed + # Link: https://github.com/garabik/grc + alias ll='grc ls -l --all --classify --group-directories-first --human-readable --color=always' + else + # Use standard long listing format + alias ll='llcolor' + fi + fi +else + alias ls='ls -aFh --color=always' # do add built-in colors to file types + alias ll='ls -Fals' # long listing + alias labc='ls -lap' # sort alphabetically + alias lx='ll -laXBh' # sort by extension + alias lk='ll -laSrh' # sort by size + alias lt='ll -latrh' # sort by date + alias lc='ll -lacrh' # sort by change time + alias lu='ll -laurh' # sort by access time + alias new='ls -latr | tail -10 | tac' # list recently created/updated files +fi + +# List all files larger than a given size +# llfs +10k will find and display all files larger than 10 kilobytes in the currect directory +alias llfs='_llfs(){ find . -type f -size "$1" -exec ls --color --classify --human-readable -l {} \; ; }; _llfs' + +# Show colors with the dir command +if hascommand --strict dir; then + alias dir='dir --color=auto --almost-all --human-readable --group-directories-first -F' + alias vdir='vdir --color=auto' +fi + +# Show the previous path +alias pwd-='echo ${OLDPWD}' + +# Show full path of file or wildcard +alias fullpath='find "`pwd`" -name' + +# List the PATH environment variable directories +alias path='echo -e ${PATH//:/\\n}' + +# When changing a directory, don't show an extra line with the directory +# Link: https://askubuntu.com/questions/1316485/how-do-i-stop-cd-command-from-printing-absolute-path-everytime +if [[ $_SILENCE_CD_OUTPUT = true ]]; then + alias cd='>/dev/null cd' +fi + +# Change to the home directory +alias home='cd ~' + +# Allow changing directory when missing a space +alias cd..='cd ..' + +# Allow changing directory back when missing a space +alias cd-='cd -' + +# Go back directories using dot style +alias ..='up 1' +alias ...='up 2' +alias ....='up 3' +alias .....='up 4' +alias ......='up 5' + +# Go back directories using dot dot number style +alias ..1='up 1' +alias ..2='up 2' +alias ..3='up 3' +alias ..4='up 4' +alias ..5='up 5' + +# Changes directories and immediately lists the contents with information +cdd() { + # Use enhancd if available, otherwise fallback to builtin cd + if type __enhancd::cd &>/dev/null; then + __enhancd::cd "$@" || return + else + builtin cd "$@" || return + fi + + # Directory contents - using user's aliases + local COUNT=$(builtin command ls -A | wc -l) + if [[ ${COUNT} -gt 20 ]]; then + ls # User's ls alias + else + ll # User's ll alias (likely ls -al) + fi + + # Directory stats (fast using find with maxdepth) + local DIRS=$(builtin command find . -maxdepth 1 -mindepth 1 -type d 2>/dev/null | wc -l) + local FILES=$((COUNT - DIRS)) + local LARGE=$(builtin command find . -maxdepth 1 -type f -size +100M 2>/dev/null | wc -l) + + # Get total size if not too many files (performance) + local SIZE_INFO="" + [[ ${COUNT} -le 100 ]] && SIZE_INFO=", total size $(builtin command du -sh . 2>/dev/null | builtin command cut -f1)" + + # Show stats and size on same line + builtin echo -e "📊 ${DIRS} dirs, ${FILES} files${SIZE_INFO}" + [[ ${LARGE} -gt 0 ]] && builtin echo " ⚠️ ${LARGE} files over 100MB" + + # Project detection - check for various project markers + local PROJECT_TYPE="" + [[ -f package.json ]] && PROJECT_TYPE="${PROJECT_TYPE}📦 Node" + [[ -f Cargo.toml ]] && PROJECT_TYPE="${PROJECT_TYPE}🦀 Rust" + [[ -f go.mod ]] && PROJECT_TYPE="${PROJECT_TYPE}🐹 Go" + [[ -f requirements.txt ]] || [[ -f setup.py ]] || [[ -f pyproject.toml ]] && PROJECT_TYPE="${PROJECT_TYPE}🐍 Python" + [[ -f pom.xml ]] || [[ -f build.gradle ]] && PROJECT_TYPE="${PROJECT_TYPE}☕ Java" + [[ -f Makefile ]] && PROJECT_TYPE="${PROJECT_TYPE}🔨 Make" + [[ -f docker-compose.yml ]] || [[ -f Dockerfile ]] && PROJECT_TYPE="${PROJECT_TYPE}🐳 Docker" + [[ -f .env ]] && PROJECT_TYPE="${PROJECT_TYPE}🔐 Env" + + # Only show project line if we detected something + [[ -n "${PROJECT_TYPE}" ]] && builtin echo "🏗️ Project: ${PROJECT_TYPE}" + + # Git status (only if git repo exists) + if [[ -d .git ]] || builtin command git rev-parse --git-dir > /dev/null 2>&1; then + # Get branch and change counts + local BRANCH=$(builtin command git branch --show-current 2>/dev/null || echo "detached") + local CHANGES=$(builtin command git status -s | wc -l) + local STAGED=$(builtin command git diff --cached --numstat | wc -l) + local UNSTAGED=$(builtin command git diff --numstat | wc -l) + local UNTRACKED=$(builtin command git ls-files --others --exclude-standard | wc -l) + + # Build compact status line + local GIT_INFO="🌿 Git: ⎇ ${BRANCH}" + [[ ${CHANGES} -gt 0 ]] && GIT_INFO="${GIT_INFO} | ${CHANGES} changes" + [[ ${STAGED} -gt 0 ]] && GIT_INFO="${GIT_INFO} (↑${STAGED} staged)" + [[ ${UNTRACKED} -gt 0 ]] && GIT_INFO="${GIT_INFO} (◆${UNTRACKED} untracked)" + + builtin echo "${GIT_INFO}" + fi +} + +####################################################### +### FIND FILES OR FILE INFORMATION +####################################################### + +# Searches for directories (can use wildcards) +# Example: finddir config +# Example: finddir "This has spaces" +alias finddir='find . -type d -iname' + +# Recursively find all files modified in the last 24 hours (current directory) +alias find24='find . -mtime -1 -ls' + +# Find all the symlinks containing search text (i.e. "/backup") +alias findlinks="find . -type l -printf '%p -> ' -exec readlink -f {} ';' | grep -E" + +# To count how many files are in your current file system location: +alias countfiles='find . -type f | wc -l' + +# To see if a command is aliased, a file, or a built-in command +alias check="type -t" + +# If the mlocate package is installed +if hascommand locate; then + # Case insensitive search and display only files present in your system + alias locate='sudo \locate -i -e' + + # Update the locate database before locating a file + # --require-visibility 0 ensures only accessible files are indexed + alias ulocate='sudo updatedb --require-visibility 0 && locate' + + # Always update the locate (mlocate) database as root + alias updatedb='sudo updatedb --require-visibility 0' + + # Display the number of matching entries + alias locount='sudo \locate -c' +fi + +####################################################### +### FILE MANAGEMENT +####################################################### + +# Safety net for changing permisions on / +alias chown='chown --preserve-root' +alias chmod='chmod --preserve-root' +alias chgrp='chgrp --preserve-root' + +# Alias to make a file executable +# A combination of the letters ugoa controls which users' access to the +# file will be changed: the user who owns it (u), other users in the +# file's group (g), other users not in the file's group (o), or all users +# (a). If none of these are given, the effect is as if (a) were given, +# but bits that are set in the umask are not affected. +alias mx='chmod a+x' + +# Make parent directories as needed +alias mkdir='mkdir -p' + +# When copying files, prompt before overwriting a file +alias cp='cp -i' + +# Copy file(s) recursively with permissions, ownership, timestamps, symbolic +# links, devices, special files, security context, and extended attributes +if hascommand --strict rsync; then + alias clone='rsync --archive --human-readable --progress' + + # Clone and synchronize directories deleting files not present at source + alias clonesync='rsync --archive --delete --human-readable --progress' +else + alias clone='cp --recursive --preserve=all --verbose' +fi + +# When moving files, prompt for confirmation if the destination path exists +# Use -f if you want to skip all prompts (-i option is ignored) +alias mv='mv -i' + +# Prompt whether to remove destinations +alias ln='ln -i' + +# Prompt before every removal +alias rm='rm -I --preserve-root' + +# Remove a directory and all files +alias rmd='command rm --recursive --force --verbose' + +# When shredding files, shred no matter permissions and remove the file(s) +alias shred='shred --force --remove --verbose' + +# Type any text into a file +alias typefile='echo "Press CTRL+d when done:" && echo "$(' + +# ExifTool removes embedded Exif data (like location) from images, videos, and docs +# https://exiftool.org/ +# https://linuxhandbook.com/remove-exif-data/ +if hascommand --strict exiftool; then + alias cleanmeta='exiftool -overwrite_original -all=' +fi + +# Create a timestamped backup of a file or directory +# Usage: bak [file_or_directory] +function bak() { + # Display help if no parameter provided + if [[ -z "${1}" ]] || [[ "${1}" == "--help" ]] || [[ "${1}" == "-h" ]]; then + echo -e "${BRIGHT_CYAN}bak${RESET}: Create a timestamped backup of a file or directory" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}bak${RESET} ${BRIGHT_MAGENTA}<${BRIGHT_YELLOW}file_or_directory${BRIGHT_MAGENTA}>${RESET}" + echo -e "${BRIGHT_WHITE}Examples:${RESET}" + echo -e " ${BRIGHT_CYAN}bak${RESET} ${BRIGHT_YELLOW}config.yml${RESET} ${BRIGHT_BLUE}# Creates config.yml.2024-03-15.bak${RESET}" + echo -e " ${BRIGHT_CYAN}bak${RESET} ${BRIGHT_YELLOW}myproject/${RESET} ${BRIGHT_BLUE}# Creates myproject.2024-03-15.bak/${RESET}" + return 1 + fi + + # Check if source exists + if [[ ! -e "$1" ]]; then + echo -e "${BRIGHT_RED}ERROR:${RESET} ${BRIGHT_CYAN}$1${RESET} does not exist" + return 1 + fi + + # Remove trailing slash for directories + local SOURCE="${1%/}" + local DATE_STAMP=$(date +"%Y-%m-%d") + local BACKUP_BASE="${SOURCE}.${DATE_STAMP}" + local BACKUP_PATH="${BACKUP_BASE}.bak" + + # If backup already exists, add incrementing number + if [[ -e "${BACKUP_PATH}" ]]; then + local COUNTER=1 + while [[ -e "${BACKUP_BASE}.${COUNTER}.bak" ]]; do + ((COUNTER++)) + done + BACKUP_PATH="${BACKUP_BASE}.${COUNTER}.bak" + fi + + # Perform the backup + local CP_STATUS + if [[ -d "${SOURCE}" ]]; then + cp -r "${SOURCE}" "${BACKUP_PATH}" + CP_STATUS=$? + else + cp "${SOURCE}" "${BACKUP_PATH}" + CP_STATUS=$? + fi + + if [[ ${CP_STATUS} -eq 0 ]]; then + echo -e "${BRIGHT_GREEN}Created backup:${RESET} ${BRIGHT_CYAN}${BACKUP_PATH}${RESET}" + else + echo -e "${BRIGHT_RED}Failed to create backup${RESET}" + return 1 + fi +} + +# Restore a backup of a file or directory +# Usage: unbak [file_or_directory] +function unbak() { + # Display help if no parameter provided + if [[ -z "${1}" ]] || [[ "${1}" == "--help" ]] || [[ "${1}" == "-h" ]]; then + echo -e "${BRIGHT_CYAN}unbak${RESET}: Restore a backup of a file or directory" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}unbak${RESET} ${BRIGHT_MAGENTA}<${BRIGHT_YELLOW}original_name${BRIGHT_MAGENTA}>${RESET}" + echo -e "${BRIGHT_WHITE}Examples:${RESET}" + echo -e " ${BRIGHT_CYAN}unbak${RESET} ${BRIGHT_YELLOW}config.yml${RESET} ${BRIGHT_BLUE}# Select from config.yml.*.bak${RESET}" + echo -e " ${BRIGHT_CYAN}unbak${RESET} ${BRIGHT_YELLOW}myproject${RESET} ${BRIGHT_BLUE}# Select from myproject.*.bak/${RESET}" + return 1 + fi + + # Remove trailing slash for directories + local TARGET="${1%/}" + + # Find all backup files sorted by modification time (most recent first) + local BACKUPS="" + if [[ "$(uname)" == "Darwin" ]]; then + # macOS: use stat with -f for format + BACKUPS=$(find . -maxdepth 1 -name "${TARGET}.*.bak" -print0 2>/dev/null | xargs -0 stat -f "%m %N" 2>/dev/null | sort -rn | cut -d' ' -f2-) + else + # Linux/GNU: use stat with --format + BACKUPS=$(find . -maxdepth 1 -name "${TARGET}.*.bak" -print0 2>/dev/null | xargs -0 stat --format="%Y %n" 2>/dev/null | sort -rn | cut -d' ' -f2-) + fi + + # Fallback: use ls if stat method failed or returned empty + if [[ -z "${BACKUPS}" ]]; then + BACKUPS=$(ls -t "${TARGET}".*.bak 2>/dev/null) + fi + + # Check if any backups were found + if [[ -z "${BACKUPS}" ]]; then + echo -e "${BRIGHT_RED}ERROR:${RESET} No backups found for ${BRIGHT_CYAN}${TARGET}${RESET}" + return 1 + fi + + # Count the number of backups + local BACKUP_COUNT=$(echo "${BACKUPS}" | wc -l) + + # Select backup to restore + local SELECTED_BACKUP + if [[ ${BACKUP_COUNT} -eq 1 ]]; then + # Only one backup exists, use it directly + SELECTED_BACKUP="${BACKUPS}" + echo -e "Found backup: ${BRIGHT_CYAN}${SELECTED_BACKUP}${RESET}" + else + # Multiple backups exist, let user choose + echo -e "Found ${BRIGHT_YELLOW}${BACKUP_COUNT}${RESET} backups for ${BRIGHT_CYAN}${TARGET}${RESET}:" + SELECTED_BACKUP=$(echo "${BACKUPS}" | createmenu) + + # Check if user canceled the selection + if [[ -z "${SELECTED_BACKUP}" ]]; then + echo "Operation canceled." + return 1 + fi + fi + + # Show what we're about to do and confirm + if [[ -e "${TARGET}" ]]; then + echo -e "${BRIGHT_YELLOW}WARNING:${RESET} ${BRIGHT_CYAN}${TARGET}${RESET} will be overwritten" + fi + + if ask "Restore from ${SELECTED_BACKUP}?" Y; then + # Remove existing target if it exists + if [[ -e "${TARGET}" ]]; then + rm -rf "${TARGET}" + fi + + # Restore the backup + local CP_STATUS + if [[ -d "${SELECTED_BACKUP}" ]]; then + cp -r "${SELECTED_BACKUP}" "${TARGET}" + CP_STATUS=$? + else + cp "${SELECTED_BACKUP}" "${TARGET}" + CP_STATUS=$? + fi + + if [[ ${CP_STATUS} -eq 0 ]]; then + echo -e "${BRIGHT_GREEN}Restored${RESET} ${BRIGHT_CYAN}${TARGET}${RESET} from ${BRIGHT_CYAN}${SELECTED_BACKUP}${RESET}" + else + echo -e "${BRIGHT_RED}Failed to restore backup${RESET}" + return 1 + fi + else + echo "Operation canceled." + fi +} + +# Show mount points in a pretty, human readable format +# NOTE: Not aliased to 'mount' because that breaks actual mount operations +alias mounts='command mount | column -t' +alias m='mounts' +alias um='umount' + +# Your Linux system's filesystem table (or fstab) is a configuration table +# designed to automate mounting and unmounting file systems to a machine +# WARNING: Modifications to this file can make your system unbootable! +alias fstab='sudo cp /etc/fstab /etc/fstab.backup; edit /etc/fstab' + +# Display disk space available and show file system type +if hascommand --strict duf; then + # duf Disk Usage/Free Utility + # Link: https://github.com/muesli/duf + # Link: https://linuxhint.com/use-duf-command-in-linux/ + #alias df='duf -hide-fs squashfs' + alias lll='duf -only local' + +elif hascommand --strict vizex; then + # Visualize disk space usage for every partition and media on the user's machine + # Link: https://github.com/bexxmodd/vizex + alias lll='vizex' + +else + # df provides valuable information on disk space utilization + # Link: https://www.geeksforgeeks.org/df-command-linux-examples/ + if hascommand --strict grc; then + alias lll='colourify \df --human-readable --print-type --exclude-type=squashfs --exclude-type=tmpfs --exclude-type=devtmpfs --exclude-type=efivarfs' + else + alias lll='command df --human-readable --print-type --exclude-type=squashfs --exclude-type=tmpfs --exclude-type=devtmpfs --exclude-type=efivarfs' + fi +fi + +# Get the block size for a partition +# Example: blocksize /dev/sda +alias blocksize='sudo blockdev --getbsz' + +# dua parallel disk space analyzer in interactive mode TUI/GUI (in color) +# Link: https://github.com/Byron/dua-cli +# Install: curl -LSfs https://raw.githubusercontent.com/byron/dua-cli/master/ci/install.sh | \sh -s -- --git byron/dua-cli --target x86_64-unknown-linux-musl --crate dua +if [[ -f "${HOME}/.cargo/bin/dua" ]]; then + alias diskspace="${HOME}/.cargo/bin/dua i" +elif hascommand dua; then + alias diskspace='dua i' +# gdu fast parallel disk usage analyzer written in Go +# Link: https://github.com/dundee/gdu +# Install: go install github.com/dundee/gdu/v5/cmd/gdu@latest +elif hascommand gdu; then + alias diskspace='gdu' +# Ncdu is a disk usage analyzer with an ncurses interface +# Link: https://dev.yorhel.nl/ncdu +elif hascommand ncdu; then + alias diskspace='ncdu' +# diskonaut gives a visual treemap of what is taking up your disk space +# Link: https://github.com/imsnif/diskonaut +elif hascommand diskonaut; then + alias diskspace='diskonaut' +# Dust is like du written in Rust and more intuitive +# Link: https://github.com/bootandy/dust +elif hascommand dust; then + alias diskspace='dust -xd1' +# List all folders disk space sorted by largest space +else + alias diskspace='du -S | sort -n -r | more' +fi + +# Just show the size of the current folder or a specified folder +alias totalsize='du -sh' + +# List disk space of immediate folders one level deep +alias folders='du -kh --max-depth=1' + +# f3 - test and check real capacity for USB devices (backup your drive first) +# Link: https://github.com/AltraMayor/f3 +# Link: https://www.linuxbabe.com/command-line/f3-usb-capacity-fake-usb-test-linux +if hascommand --strict f3probe; then + alias usbtest='sudo f3probe --destructive --time-ops' +fi + +# Alias to show MySQL's data directory location +if hascommand --strict mysqld; then + alias mysqldatadir='mysqld --verbose --help 2>/dev/null | grep ^datadir | column --table' +fi + +# Alias to launch a document, file, or URL in it's default X application +if hascommand --strict xdg-open; then + alias open='runfree xdg-open' +fi + +# Source fzf if installed via Git +[ -f ~/.fzf.bash ] && builtin source ~/.fzf.bash # Source if installed via Git + +# Alias to fuzzy find files in the current folder(s), preview them, and launch in an editor +if hascommand --strict fzf; then + # Preview text files in a directory + if hascommand --strict xdg-open; then + preview() { + open $(fzf --info=inline --query="${1}" --preview="$PAGER {}") + } + else + preview() { + edit $(fzf --info=inline --query="${1}" --preview="$PAGER {}") + } + fi + + # Find log files using file previews + if [[ -d /var/log ]]; then + # Alias to find and view log files + if hascommand bat; then + # Use bat for preview if available for better syntax highlighting + alias findlog='(cd /var/log && _LOG_SELECTED=$(sudo \find . -maxdepth 10 -iname "*.log" | \sort -f | sudo fzf --layout=reverse-list --info=inline --preview="sudo bat --color=always {}"); if [ -n "${_LOG_SELECTED}" ]; then echo -e "${BRIGHT_CYAN}Log file: ${BRIGHT_YELLOW}$(sudo \realpath "${_LOG_SELECTED}")${RESET}"; logview "$(sudo \realpath "${_LOG_SELECTED}")"; fi)' + else + # Fall back to the default PAGER + alias findlog='(cd /var/log && _LOG_SELECTED=$(sudo \find . -maxdepth 10 -iname "*.log" | \sort -f | sudo -E fzf --layout=reverse-list --info=inline --preview="sudo ${PAGER} {}"); if [ -n "${_LOG_SELECTED}" ]; then echo -e "${BRIGHT_CYAN}Log file: ${BRIGHT_YELLOW}$(sudo \realpath "${_LOG_SELECTED}")${RESET}"; logview "$(sudo \realpath "${_LOG_SELECTED}")"; fi)' + fi + fi +fi + +# Alias for Midnight Commander (mc) to exit into current directory +# NOTE: Use mcc for the shell command line features as the subshell is very slow +# https://stackoverflow.com/questions/39017391/how-to-make-midnight-commander-exit-to-its-current-directory +# https://unix.stackexchange.com/questions/57439/slow-start-of-midnight-commander +# https://midnight-commander.org/ticket/3580 +if [[ -f "/usr/lib/mc/mc-wrapper.sh" ]]; then + alias mcc='builtin source /usr/lib/mc/mc-wrapper.sh' + alias mc='builtin source /usr/lib/mc/mc-wrapper.sh --nosubshell' +elif hascommand --strict mc; then + alias mcc='command mc' + alias mc='command mc --nosubshell' +fi + +# Check shell script syntax +if hascommand --strict shellcheck; then + alias schk='shellcheck' +fi + +# Alias to view log files in real time (usually in /var/log) +# Check for lnav (The Log File Navigator) +# Link: https://lnav.org +if hascommand --strict lnav; then + alias logview='sudo \lnav -r -t' + alias logs='sudo \lnav -r -t /var/log/*.log' # All logs in /var/log + +# Check for multitail (Multiple Tail) +# Link: https://www.vanheusden.com/multitail/ +elif hascommand --strict multitail; then + alias logview='sudo \multitail -c -s 2' + alias logs='sudo \multitail -c -s 2 /var/log/*.log' + +# Check for tail +elif hascommand --strict tail; then + alias logview='sudo tail -f' + alias logs="find /var/log -type f -exec file {} \; | grep 'text' | cut -d' ' -f1 | sed -e's/:$//g' | grep -v '[0-9]$' | xargs tail -f" + +# If no log viewer is installed, use less as a fallback +else + alias logview='sudo \less +F' +fi + +# Show logs in color +if hascommand --strict multitail; then + alias multitail='command multitail -c' +fi + +####################################################### +### DATE AND TIME +####################################################### + +# Show the time +alias now='date +"%T"' + +# Show the short date +alias today='date +"%Y-%m-%d"' + +# Stop watch +alias stopwatch='date && echo "Press CTRL+D to stop" && time read' + +# Countdown timer and stop watch +# Link: https://github.com/trehn/termdown +# Fonts: banner3, big, computer, doh, letters, roman, small, standard, univers +# Link: http://www.figlet.org/examples.html +if hascommand --strict termdown; then + alias termdown='termdown --blink --end --critical 10 --font doh' + alias countdown="termdown --help | sed -n '/Options\:/q;p'" + alias timer='termdown' + alias clock='termdown --time' +fi + +# Change a file's (or files using a wildcard) accessed and modified time to now +# NOTE: There is no file creation date in Unix, only access, modify, and change +alias filetimenow='touch -a -m' + +####################################################### +### CPU, MEMORY, AND PROCESSES +####################################################### + +# Display amount of free and used memory +alias free='free -h' + +# When reporting a snapshot of the current processes: +# a = all users +# u = user-oriented format providing detailed information +# x = list the processes without a controlling terminal +# f = display a tree view of parent to child processes +#alias ps='ps auxf' + +# Show top ten processes +alias cpu='ps aux | sort -r -nk +4 | head | $PAGER' + +# Show CPU information +alias cpuinfo='lscpu | $PAGER' + +# Show the USB device tree +if hascommand --strict lsusb; then + alias usb='lsusb -t' +fi + +# Show the PCI device tree +if hascommand --strict lspci; then + alias pci='lspci -tv' +fi + +# Alias top +# Link: https://ostechnix.com/some-alternatives-to-top-command-line-utility-you-might-want-to-know/ +# Link: https://www.linuxlinks.com/alternativestotop/ +# List of preferred top commands in order of preference + +# Iterate over a list of preferred top commands in order of preference/features +_TOP_COMMANDS=("btop" "bpytop" "bashtop" "nmon" "glances" "ytop" "gtop" "htop") +for _TOP_COMMAND in "${_TOP_COMMANDS[@]}"; do + # Check if the command exists and is executable + if hascommand --strict "${_TOP_COMMAND}"; then + # Create an alias for the 'top' command using the found command + alias top="${_TOP_COMMAND}" + # Exit the loop once the first matching command is found + break + fi +done +# Clean up and don't leave the extra variables in the environment +unset _TOP_COMMANDS +unset _TOP_COMMAND + +# Alias bottom +# Link: https://github.com/ClementTsang/bottom +if hascommand --strict btm; then + alias bottom='btm' +fi + +# nvtop is a task monitor for NVIDIA, AMD and Intel GPUs +# Link: https://github.com/Syllo/nvtop +if hascommand --strict nvtop; then + alias gpu='nvtop' +fi + +# Show jobs +alias j='jobs -l' + +# Change the cursor to a crosshair to select a window (requires xprop) +if hascommand --strict xprop; then + alias windowinfo='xprop' +fi + +# Get active X-window process ID after a 3 second delay (requires xdotool) +if hascommand --strict xdotool; then + alias activewinpid='sleep 3 && xdotool getactivewindow getwindowpid' +fi + +# Alias to clear RAM memory cache, buffer and swap space +# Link: https://www.tecmint.com/clear-ram-memory-cache-buffer-and-swap-space-on-linux/ +alias flushcache="sudo free -h && \\ + sudo su -c \"echo 3 >'/proc/sys/vm/drop_caches' && \\ + swapoff -a && \\ + swapon -a && \\ + printf '\n${BRIGHT_YELLOW}%s${RESET}\n\n' 'Ram-cache and Swap Cleared'\" root && \\ + free -h" + +####################################################### +### NETWORKING +####################################################### + +# yt-dlp fork of youtube-dl - Set the default download folder +# Link: https://github.com/yt-dlp/yt-dlp +if hascommand --strict yt-dlp; then + alias yt-dlp='yt-dlp' + alias ytd='yt-dlp' +# Youtube-dl - Use best settings +# Link: https://github.com/ytdl-org/youtube-dl +elif hascommand --strict youtube-dl; then + alias youtube-dl="youtube-dl --format 'best[vcodec*=avc]'" + alias ytd='youtube-dl' +fi + +# Checking for the presence of download utilities to create a download alias +# and an optional alias for downloading the LS_COLORS file to ~/.dircolors +# that requires the dircolors command used by ls to set color directory output +if hascommand aria2c; then + # aria2c is a lightweight multi-protocol & multi-source command-line + # download utility that supports HTTP/HTTPS, FTP, SFTP, BitTorrent, and + # Metalink with multiple connections and enhanced control over connections + # Link: https://aria2.github.io/ + alias aria2c='aria2c --max-connection-per-server=5 --continue=true --async-dns=false' + + if hascommand --strict dircolors; then + alias download-dircolors='aria2c --max-connection-per-server=5 --continue=true --async-dns=false -d "${HOME}" -o .dircolors https://raw.githubusercontent.com/ahmadassaf/dircolors/master/LS_COLORS' + fi +elif hascommand --strict wget; then + # wget is a non-interactive command-line file downloader for HTTP, HTTPS, + # and FTP that supports resuming downloads on more unstable connections + # Link: https://www.gnu.org/software/wget/ + if hascommand --strict dircolors; then + alias download-dircolors='wget --continue -O "${HOME}/.dircolors" https://raw.githubusercontent.com/ahmadassaf/dircolors/master/LS_COLORS' + fi +elif hascommand --strict curl; then + # curl supports data transfer from or to a server using multiple protocols + # like HTTP, HTTPS, and FTP, and features resuming and redirect following + # Link: https://curl.se/ + if hascommand --strict dircolors; then + alias download-dircolors='curl -C - -L -o "${HOME}/.dircolors" https://raw.githubusercontent.com/ahmadassaf/dircolors/master/LS_COLORS' + fi +fi + +# Resume wget by default +if hascommand --strict wget; then + alias wget='wget -c' +fi + +# Stop pinging after sending 5 ECHO_REQUEST packets +alias ping='ping -c 5' + +# Do not wait for ping interval 1 second, go fast +alias fastping='ping -c 100 -i.2' + +# Show open ports +alias ports='netstat -tulanp' + +# Display and monitor the disk IO usage +# Link: https://www.geeksforgeeks.org/iotop-command-in-linux-with-examples/ +if hascommand iotop; then + alias iotop='sudo iotop -o -a' +fi + +# If nmap is installed, set an alias for a network scan of a host (takes a while) +# Scan delay slows things down but reduces throttling, anti-ddos, auto-block +# Link: https://nmap.org/ +# Example: netscan localhost +if hascommand nmap; then + alias netscan='sudo nmap --scan-delay 1.1s -v --resolve-all -A -sTUV' +fi + +# Get local IP addresses +if hascommand --strict ip; then + alias iplocal="ip -br -c a" +else + alias iplocal="ifconfig | grep -Eo 'inet (addr:)?([0-9]*\.){3}[0-9]*' | grep -Eo '([0-9]*\.){3}[0-9]*' | grep -v '127.0.0.1'" +fi + +# Get public IP address (several options below) +alias ipexternal='wget -O - -q icanhazip.com && echo' +# alias ipexternal='wget -qO- ifconfig.me/ip && echo' +# alias ipexternal='curl ipinfo.io/ip && echo' + +# Make it easy to disable and reenable the Teamviewer service +# so that it's not constantly running and only when you need it +if hascommand --strict teamviewer; then + alias teamviewerstart='sudo teamviewer --daemon start' + alias teamviewerstop='sudo teamviewer --daemon stop' +fi + +# Check SSL certificate expiration for a domain +# Usage: sslcheck [domain] [port] +function sslcheck() { + # Display help if no parameter provided + if [[ -z "${1}" ]] || [[ "${1}" == "--help" ]] || [[ "${1}" == "-h" ]]; then + echo -e "${BRIGHT_CYAN}sslcheck${RESET}: Check SSL certificate expiration for a domain" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}sslcheck${RESET} ${BRIGHT_MAGENTA}<${BRIGHT_YELLOW}domain${BRIGHT_MAGENTA}>${RESET} ${BRIGHT_MAGENTA}[${BRIGHT_GREEN}port${BRIGHT_MAGENTA}]${RESET}" + echo -e "${BRIGHT_WHITE}Examples:${RESET}" + echo -e " ${BRIGHT_CYAN}sslcheck${RESET} ${BRIGHT_YELLOW}google.com${RESET} ${BRIGHT_BLUE}# Uses default port 443${RESET}" + echo -e " ${BRIGHT_CYAN}sslcheck${RESET} ${BRIGHT_YELLOW}example.com 8443${RESET} ${BRIGHT_BLUE}# Custom port${RESET}" + return 1 + fi + + # Check if openssl is available + if ! hascommand --strict openssl; then + echo -e "${BRIGHT_RED}ERROR:${RESET} openssl is required but not installed" + return 1 + fi + + local DOMAIN="$1" + local PORT="${2:-443}" + + # Remove protocol prefix if provided + DOMAIN="${DOMAIN#https://}" + DOMAIN="${DOMAIN#http://}" + # Remove trailing slash and path + DOMAIN="${DOMAIN%%/*}" + + echo -e "Checking SSL certificate for ${BRIGHT_CYAN}${DOMAIN}:${PORT}${RESET}..." + + # Get certificate expiry date + local CERT_INFO + CERT_INFO=$(echo | openssl s_client -servername "${DOMAIN}" -connect "${DOMAIN}:${PORT}" 2>/dev/null | openssl x509 -noout -dates 2>/dev/null) + + if [[ -z "${CERT_INFO}" ]]; then + echo -e "${BRIGHT_RED}ERROR:${RESET} Could not retrieve certificate from ${BRIGHT_CYAN}${DOMAIN}:${PORT}${RESET}" + echo -e "Possible causes: domain doesn't exist, no SSL, connection refused, or timeout" + return 1 + fi + + # Extract the expiry date + local EXPIRY_DATE + EXPIRY_DATE=$(echo "${CERT_INFO}" | grep 'notAfter=' | cut -d= -f2) + + if [[ -z "${EXPIRY_DATE}" ]]; then + echo -e "${BRIGHT_RED}ERROR:${RESET} Could not parse certificate expiry date" + return 1 + fi + + # Calculate days until expiry (cross-platform) + local EXPIRY_EPOCH + local NOW_EPOCH + local DAYS_LEFT + + # Normalize date string (handle double spaces in day) + EXPIRY_DATE=$(echo "${EXPIRY_DATE}" | sed 's/ */ /g') + + if [[ "$(uname)" == "Darwin" ]]; then + # macOS date syntax + EXPIRY_EPOCH=$(date -j -f "%b %d %T %Y %Z" "${EXPIRY_DATE}" "+%s" 2>/dev/null) + else + # Linux/GNU date syntax + EXPIRY_EPOCH=$(date -d "${EXPIRY_DATE}" "+%s" 2>/dev/null) + fi + + NOW_EPOCH=$(date "+%s") + + if [[ -n "${EXPIRY_EPOCH}" ]]; then + DAYS_LEFT=$(( (EXPIRY_EPOCH - NOW_EPOCH) / 86400 )) + else + # Fallback: just show the date without days calculation + echo -e "Certificate expires: ${BRIGHT_YELLOW}${EXPIRY_DATE}${RESET}" + echo -e "${BRIGHT_YELLOW}(Could not calculate days remaining)${RESET}" + return 0 + fi + + # Display result with color based on urgency + local COLOR + local STATUS + if [[ ${DAYS_LEFT} -lt 0 ]]; then + COLOR="${BRIGHT_RED}" + STATUS="EXPIRED" + echo -e "${COLOR}Certificate ${STATUS}${RESET} $((-DAYS_LEFT)) days ago" + elif [[ ${DAYS_LEFT} -lt 7 ]]; then + COLOR="${BRIGHT_RED}" + STATUS="CRITICAL" + echo -e "${COLOR}${STATUS}:${RESET} Certificate expires in ${COLOR}${DAYS_LEFT}${RESET} days" + elif [[ ${DAYS_LEFT} -lt 30 ]]; then + COLOR="${BRIGHT_YELLOW}" + STATUS="WARNING" + echo -e "${COLOR}${STATUS}:${RESET} Certificate expires in ${COLOR}${DAYS_LEFT}${RESET} days" + else + COLOR="${BRIGHT_GREEN}" + echo -e "${COLOR}OK:${RESET} Certificate expires in ${COLOR}${DAYS_LEFT}${RESET} days" + fi + + echo -e "Expiry date: ${BRIGHT_CYAN}${EXPIRY_DATE}${RESET}" +} + +####################################################### +### VISUAL +####################################################### + +# Colorize the grep command output for ease of use (good for log files) +alias grep='grep --color=auto' +alias egrep='egrep --color=auto' +alias fgrep='fgrep --color=auto' + +# Show the currectdisplay server +alias whichdisplay='echo -e "\033[1;33m${XDG_SESSION_TYPE^}\033[0m"' + +# Colorize messages for the kernel ring buffer +alias dmesg='dmesg --color' + +# Clear the screen with both clear and cls shorthand +alias cls='clear' + +# Search and play YouTube videos in the terminal +if hascommand --strict ytfzf; then + alias yt='ytfzf -l -t' +fi + +# Convert images to ASCII in color +if hascommand --strict jp2a; then + alias jp2a='jp2a --color' +fi + +# List fonts for toilet +if hascommand --strict toilet; then + alias toiletfont='ls /usr/share/figlet' + alias toiletfontlist='for file in /usr/share/figlet/*.?lf; do toilet -f $(basename "$file") $(basename "$file"); done' +fi + +# Launch the cmatrix screen saver (if installed) +if hascommand --strict cmatrix; then + alias matrix='cmatrix -b' +fi + +# Wrap text breaking at whitespace only +# use -w or --width=WIDTH to set line width (default 80) +if hascommand --strict fold; then + alias wrap='fold --spaces' +fi + +# Fix spacing and reformat text while preserving paragraphs +if hascommand --strict fmt; then + alias unwrap='fmt' +fi + +####################################################### +### FILE SYSTEMS +####################################################### + +# Aliases for BtrFS file systems +if hascommand --strict btrfs; then + alias btrcheck='sudo btrfs fi show' # Check status of raid drives + alias btrstats='sudo btrfs device stats' # Show device statistics + alias btrscrub='sudo btrfs scrub start' # Start a scrub + alias btrpause='sudo btrfs scrub cancel' # Cancel or pause a scrub + alias btrresume='sudo btrfs scrub resume' # Resume a paused scrub + alias btrstatus='sudo btrfs scrub status' # Show status of a scrub + alias btrdefragfile='sudo btrfs filesystem defragment -vf' # Defrag a file + alias btrdefragdir='sudo btrfs filesystem defragment -rvf' # Defrag a directory +fi + +# Aliases for Ext3 file systems +if hascommand --strict mkfs.ext3; then + alias ext3check='sudo fsck.ext3' # Check and repair Ext3 filesystem + alias ext3stats='sudo dumpe2fs -h' # Show statistics for Ext3 filesystem + alias ext3trim='sudo fstrim -v' # Trim unused blocks on Ext3 filesystem +fi + +# Aliases for EXT4 file systems +if hascommand --strict mkfs.ext4; then + alias ext4check='sudo fsck.ext4' # Check and repair EXT4 filesystem + alias ext4stats='sudo dumpe2fs -h' # Show statistics for EXT4 filesystem + alias ext4trim='sudo fstrim -v' # Trim unused blocks on EXT4 filesystem +fi + +# Aliases for F2FS file systems +if hascommand --strict mkfs.f2fs; then + alias f2fscheck='sudo fsck.f2fs' # Check and repair F2FS filesystem + alias f2fsstats='sudo dump.f2fs' # Show statistics for F2FS filesystem + alias f2fstrim='sudo fstrim -v' # Trim unused blocks on F2FS filesystem +fi + +# Aliases for XFS file systems +if hascommand --strict mkfs.xfs; then + alias xfscheck='sudo xfs_repair' # Check and repair XFS filesystem + alias xfsstats='sudo xfs_info' # Show information about XFS filesystem + alias xfstrim='sudo fstrim -v' # Trim unused blocks on XFS filesystem +fi + +# Aliases for ZFS file systems +if hascommand --strict zpool; then + alias zfscheck='sudo zpool scrub' # Check and repair ZFS pool + alias zfsstats='sudo zpool iostat' # Show ZFS pool statistics + alias zfstrim='sudo zpool trim' # Trim unused blocks on ZFS pool + alias zfsstatus='sudo zpool status' # Check status of ZFS pool + alias zfsscrub='sudo zpool scrub' # Start a scrub on ZFS pool + alias zfspause='sudo zpool scrub -s' # Pause a scrub on ZFS pool + alias zfsresume='sudo zpool scrub' # Resume a paused scrub (just re-run scrub) + alias zfsscrubstatus='sudo zpool status -v' # Show status of ZFS pool scrub + # NOTE: ZFS does not support traditional defragmentation. + # 'zfs send -R' sends a replication stream (raw binary) to stdout -- not defrag. +fi + +# Aliases for NTFS filesystems +if hascommand --strict ntfsfix; then + alias ntfscheck='sudo ntfsfix' # Check and repair NTFS filesystem + alias ntfsstats='sudo ntfsinfo' # Show information about NTFS filesystem +fi + +# Aliases for exFAT, FAT12, FAT16, and FAT32 filesystems +# I'm on a diet, so no jokes please. This whole section. lol +if hascommand --strict dosfsck; then + alias fatcheck='sudo dosfsck' # Check and repair FAT filesystem + alias fatstats='sudo fsck.fat -i' # Show information about FAT filesystem +fi + +####################################################### +### DISTROBOX +####################################################### + +# If Distrobox is installed... +# Link: https://github.com/89luca89/distrobox +# Link: https://github.com/89luca89/distrobox/blob/main/docs/usage/usage.md +if hascommand --strict distrobox; then + # Create aliases for the most common commands + alias db='distrobox' + alias {dbe,distrobox-enter}='_distrobox-enter' + alias dbl='distrobox list' + alias dbls='distrobox-list-simple' + alias dbs='distrobox stop' + alias dbsa='distrobox-stop-all' + alias dbhe='distrobox-host-exec' + alias {dbup,distrobox-upgrade}='_distrobox-upgrade' + alias {dbc,distrobox-check}='docker system df -v' + + # List only the names of each container + # This takes no arguments and will mostly be used in scripts + function distrobox-list-simple() { + distrobox-list | awk -F '|' '{print $2}' | tail -n +2 | sed 's/^[ \t]*//;s/[ \t]*$//' + } + + # Choose an installed container + # This takes no arguments and will mostly be used in scripts + function distrobox-pick() { + distrobox-list-simple | createmenu + } + + # Loop through and stop all containers + function distrobox-stop-all() { + local _BOX + while IFS= read -r _BOX; do + [[ -n "${_BOX}" ]] && \distrobox stop --yes "${_BOX}" + done <<< "$(distrobox-list-simple)" + } + + function _distrobox-enter() { + if [ $# -eq 0 ]; then + command distrobox enter "$(distrobox-pick)" + else + command distrobox enter "$@" + fi + } + + function _distrobox-upgrade() { + if [ $# -eq 0 ]; then + command distrobox upgrade --all + else + command distrobox upgrade "$@" + fi + } +fi + +####################################################### +### MISCELLANEOUS +####################################################### + +# Raw kernel ring buffer messages +alias kernelmessages='sudo dmesg' + +# Filter kernel messages for problems +alias kernelerrors='sudo dmesg | grep -iE "error|fail|warn|critical|panic|oops|segfault|timeout|refused|denied|unable|cannot|invalid|corrupt|bad|broken|missing|not found|no such|permission denied"' + +# Alias to show the current TTY (CTRL+ALT+1 through 7) +alias whichtty='tty | sed -e "s:/dev/::"' + +# Conditionally set alias for checking failed login attempts +if hascommand aureport; then + # Use aureport to generate a report of failed authentication attempts + alias checkloginfailures='sudo \aureport -au --failed | command less' +elif hascommand lastb; then + # If aureport is not available, check for the lastb command + alias checkloginfailures='sudo \lastb | command less' +else + # If neither executable command is available, check for system log files + if [[ -f /var/log/auth.log ]]; then + # Use grep to search for 'FAILED LOGIN' entries in auth.log + alias checkloginfailures="sudo \grep 'FAILED LOGIN' /var/log/auth.log | command less" + elif [[ -f /var/log/secure ]]; then + # Use grep to search for 'FAILED LOGIN' entries in secure + alias checkloginfailures="sudo \grep 'FAILED LOGIN' /var/log/secure | command less" + #else + # # Provide feedback if no methods are available for checking login failures + # echo "Error: No common methods or logs found for checking login failures" + fi +fi + +# Spell check a word with DidYouMean +# Link: https://github.com/hisbaan/didyoumean +if hascommand --strict dym; then + alias spell='dym -n 10 -y' +fi + +# Alias's for safe and forced reboots +alias rebootsafe='sudo shutdown -r now' +alias rebootforce='sudo shutdown -r -n now' + +# If OpenSSL is installed... +if hascommand --strict openssl; then + # If base64 isn't available, use openssl's version to do the same thing + if ! hascommand --strict base64; then + alias base64='openssl base64' + fi + + # If base64 isn't available, use openssl's version to do the same thing + if ! hascommand --strict md5sum; then + alias md5sum='openssl md5' + fi + + # SHA1 + alias sha1='openssl sha1' +fi + +# Update the fireware on Linux automatically and safely using fwupdmgr +# (used by companies like Corsair, Dell, HP, Intel, Logitech, etc.) +# Install: pkginstall fwupdmgr +# Link: https://fwupd.org +if hascommand --strict fwupdmgr; then + alias firmwareupdate='fwupdmgr get-devices && fwupdmgr refresh && fwupdmgr get-updates && fwupdmgr update' +fi + +# When updating virus definitions, do it as root +if hascommand --strict freshclam; then + alias freshclam='sudo freshclam' +fi + +# Check passwords with cracklib +# Link: https://www.cyberciti.biz/security/linux-password-strength-checker/ +if hascommand --strict cracklib-check; then + alias pwcheck='cracklib-check<<<' +fi + +# Check for the availability of web browsers +# Link: https://www.geeksforgeeks.org/using-lynx-to-browse-the-web-from-the-linux-terminal/ +# Link: https://wiki.archlinux.org/title/ELinks +# Link: https://w3m.sourceforge.net/ +# Link: http://www.aboutlinux.info/2007/02/links2-cross-platform-console-based-web.html +# Link: https://www.tecmint.com/command-line-web-browsers/ +for TERMINAL_BROWSER in "w3m" "lynx" "elinks" "links2" "links"; do + if hascommand "${TERMINAL_BROWSER}"; then + # Show the Extreme Ultimate .bashrc README file in the available browser + if [[ -f "${BASHRC_INSTALL_DIR}/README.html" ]]; then + alias readme="${TERMINAL_BROWSER} ${BASHRC_INSTALL_DIR}/README.html" + break # Exit the loop once the first available browser is found + elif [[ -f "${HOME}/README.html" ]]; then + alias readme="${TERMINAL_BROWSER} ~/README.html" + break # Exit the loop once the first available browser is found + fi + fi +done + +# If the readme alias is still not set, try markdown readers +if [[ $(type -t readme) != 'alias' ]]; then + # Check for the availability of markdown viewers + # Link: https://github.com/Textualize/frogmouth + # Link: https://github.com/charmbracelet/glow + # Link: https://github.com/swsnr/mdcat + # Link: https://github.com/ttscoff/mdless + for TERMINAL_MARKDOWN_VIEWER in "frogmouth" "glow" "mdcat" "mdless"; do + if hascommand "${TERMINAL_MARKDOWN_VIEWER}"; then + # If README.md exists in .config/bashrc, set 'readme' alias + if [[ -f "${BASHRC_INSTALL_DIR}/README.md" ]]; then + alias readme="${TERMINAL_MARKDOWN_VIEWER} ${BASHRC_INSTALL_DIR}/README.md" + break # Exit the loop once the first available browser is found + # If README.md exists in home, set 'readme' alias + elif [[ -f "${HOME}/README.md" ]]; then + alias readme="${TERMINAL_MARKDOWN_VIEWER} ~/README.md" + break # Exit the loop once the first available browser is found + fi + fi + done +fi + +# If glow is installed, set up some aliases for it +# Link: https://github.com/charmbracelet/glow +if hascommand glow; then + # Use glow's pager option + alias glow='glow --all --pager' + + # Local only version for security + alias glowsafe='glow --all --pager --local' +fi + +# If mdcat is installed, use pagination +# Link: https://github.com/swsnr/mdcat +if hascommand mdcat; then + alias mdcat='mdcat --paginate' +fi + +# jless is a command-line JSON viewer +# Link: https://jless.io/ +if hascommand --strict jless; then + alias json='jless' + +# jnv - Interactive JSON viewer and jq filter editor +# Link: https://github.com/ynqa/jnv +elif hascommand --strict jnv && hascommand --strict jq; then + alias json='jnv' + +# fx is a JavaScript Object Notation (JSON) viewer +# Link: https://github.com/antonmedv/fx +elif hascommand --strict fx; then + alias json='fx' + +# jq - Pretty Print JSON Files in the terminal +# Link: https://itsfoss.com/pretty-print-json-linux/ +elif hascommand --strict jq; then + alias json='jq' +fi + +# Pretty print JSON +if hascommand --strict jq; then + alias jsonformat='jq' +fi + +# baca TUI ebook reader +# Link: https://github.com/wustho/baca +# Install: pip install baca +if hascommand --strict baca; then + alias ebook=baca +fi + +if hascommand RHVoice-test; then + # RHVoice is a free and open-source multilingual speech synthesizer + # Link: https://rhvoice.org + alias say='RHVoice-test --rate 130 --volume 100 --profile lyubov <<<' + alias {saycb,sayclipboard}='clipboard | RHVoice-test --rate 130 --volume 100 --profile lyubov' + alias saygreet='echo "$(HOUR=$(date +%H); echo "$( [ "$HOUR" -lt 12 ] && echo "Good morning" || { [ "$HOUR" -lt 17 ] && echo "Good afternoon" || { [ "$HOUR" -lt 21 ] && echo "Good evening" || echo "Good night"; }; }) $(getent passwd "${USER}" | cut -d ":" -f 5 | cut -d "," -f 1 || echo "${USER}")! It is $(date +"%-I. %M. %p" | sed "s/AM/A. M./; s/PM/P. M./"). $(grep -q "^To " /var/mail/${USER} 2>/dev/null && echo "You have new messages." || echo "")")" | RHVoice-test --rate 130 --volume 100 --profile lyubov' +elif hascommand espeak; then + # Speak with female voice + # Link: https://thomashunter.name/posts/2012-05-21-female-voice-using-espeak + alias say='espeak -ven-us+f4 -s170' + alias {saycb,sayclipboard}='clipboard | espeak -ven-us+f4 -s170' + alias saygreet='echo "$(HOUR=$(date +%H); echo "$( [ "$HOUR" -lt 12 ] && echo "Good morning" || { [ "$HOUR" -lt 17 ] && echo "Good afternoon" || { [ "$HOUR" -lt 21 ] && echo "Good evening" || echo "Good night"; }; }) $(getent passwd "${USER}" | cut -d ":" -f 5 | cut -d "," -f 1 || echo "${USER}")! It is $(date +"%-I. %M. %p" | sed "s/AM/A. M./; s/PM/P. M./"). $(grep -q "^To " /var/mail/${USER} 2>/dev/null && echo "You have new messages." || echo "")")" | espeak -ven-us+f4 -s170' +fi + +# Aliases to modify GRUB +# https://www.howtogeek.com/196655/how-to-configure-the-grub2-boot-loaders-settings/ +alias grubedit='edit /etc/default/grub' +alias grubsave='sudo update-grub' + +# Shows all the script files in a directory and which shell they require +alias scanscripts='grep -E -r "^#!/" ./* 2> /dev/null' + +# Vlock - lock all terminals +# Install: sudo apt install vlock +# Install: sudo pacman -S kbd +# https://odysee.com/@DistroTube:2/lock-your-terminal-sessions-with-vlock:0 +if hascommand --strict vlock; then + # Alias Vlock to lock all terminals and can be typed with one hand + alias lok='vlock --all' +fi + +# Alias thefuck that corrects errors in previous console commands +# Link: https://github.com/nvbn/thefuck +if hascommand --strict thefuck; then + eval "$(thefuck --alias fix)" +fi + +# Alias to restart KDE Plasma desktop without rebooting or logging out +if hascommand --strict kstart5 && hascommand --strict kquitapp5; then + alias {plasmarestart,kderestart}='kquitapp5 plasmashell && runfree kstart5 plasmashell' +fi + +####################################################### +# Set compression aliases with automatic parallel processing if installed +# Note: Use [-P] to preserve full paths +# To encrypt the output, pipe it to the built-in encrypt function: +# mk... - [files_or_directories_to_compress] | encrypt [encrypted_file.ext.gpg] +# Example: mkgz - file1 file2 dir1 | encrypt archive.tar.gz.gpg +# Usage: +# mkbz2 [-P] [archive.tar.bz2] [files_or_directories_to_compress] +# Create a bz2 compressed tar archive (using pbzip2 if available) +# unbz2 [-P] [archive.tar.bz2] +# Extract a bz2 compressed tar archive (using pbzip2 if available) +# mkgz [-P] [archive.tar.gz] [files_or_directories_to_compress] +# Create a gz compressed tar archive (using pigz if available) +# ungz [-P] [archive.tar.gz] +# Extract a gz compressed tar archive (using pigz if available) +# mkxz [-P] [archive.tar.xz] [files_or_directories_to_compress] +# Create an xz compressed tar archive (using pixz if available) +# unxz [-P] [archive.tar.xz] +# Extract an xz compressed tar archive (using pixz if available) +# mkzst [-P] [archive.tar.zst] [files_or_directories_to_compress] +# Create a zstd compressed tar archive (using all CPU cores) +# unzst [-P] [archive.tar.zst] +# Extract a zstd compressed tar archive (using all CPU cores) +# mkzip [archive.zip] [files_or_directories_to_compress] +# Create a zip archive with maximum compression +# unzip [archive.zip] +# Extract a zip archive +# mk7z [archive.7z] [files_or_directories_to_compress] +# Create a 7z archive with ultra compression and multi-threading +# un7z [archive.7z] +# Extract a 7z archive (supports many formats) +# mktar [-P] [archive.tar] [files_or_directories_to_compress] +# Create an uncompressed tar archive +# untar [-P] [archive.tar] +# Extract an uncompressed tar archive +# mkiso [archive.iso] [files_or_directories_to_compress] +# Create an ISO image +# uniso [archive.iso] +# Extract an ISO image +####################################################### + +# Check for pbzip2 parallel block-sorting file compressor +# Link: https://github.com/ruanhuabin/pbzip2 +# Note: bzip2 can reduce files by 10-15% more than gzip on average but is slower +# and has better integrity checking even if slightly less common +if hascommand --strict pbzip2; then + alias mkbz2='tar --use-compress-program="pbzip2 -9" -cvf' + alias unbz2='tar --use-compress-program=pbzip2 -xvf' +else + alias mkbz2='tar --use-compress-program="bzip2 -9" -cvf' + alias unbz2='tar -xvjf' +fi + +# Check for pigz parallel implementation of the gzip file compressor +# Link: https://zlib.net/pigz/ +# Note: gzip is significantly faster for both compression and decompression +# and is more widely supported (virtually universal) +if hascommand --strict pigz; then + alias mkgz='tar --use-compress-program="pigz -9" -cvf' + alias ungz='tar --use-compress-program=pigz -xvf' +else + alias mkgz='tar -cvzf' + alias ungz='tar -xvzf' +fi + +# Check for pixz (parallel xz) which automatically indexes during compression +# Link: https://github.com/vasi/pixz +if hascommand --strict pixz; then + alias mkxz='tar --use-compress-program="pixz -9" -cvf' + alias unxz='tar --use-compress-program=pixz -xvf' +elif hascommand --strict xz; then + alias mkxz='tar --use-compress-program="xz -T0 -9" -cvf' + alias unxz='tar -xvJf' +fi + +# Check for zstd with better compression ratios and built-in multi-threading +# Link: https://github.com/facebook/zstd +# Note: Generally considered one of the best modern compression tools +if hascommand --strict zstd; then + alias mkzst='tar --use-compress-program="zstd -T0 -19" -cvf' + alias unzst='tar --use-compress-program="zstd -T0" -xvf' +fi + +# Check for zip compression utility (unzip is included and not needed to alias) +# Link: http://infozip.sourceforge.net/Zip.html +# Note: Best for greatest multi-platform compatibility +if hascommand --strict zip; then + alias mkzip='zip -9r' +fi + +# Check for 7-zip (p7zip) compression utility +# Link: https://sourceforge.net/projects/p7zip/ +# Note: Excellent compression ratio and supports many formats +if hascommand --strict 7z; then + alias mk7z='7z a -mx=9 -mmt=on' + alias un7z='7z x' + alias uniso='7z x' +fi + +# ISO image creation and extraction +if hascommand --strict mkisofs; then + alias mkiso='mkisofs -o' +elif hascommand --strict genisoimage; then + alias mkiso='genisoimage -o' +fi + +# Standard tar without compression +alias mktar='tar -cvf' # Create tar file +alias untar='tar -xvf' # Extract tar file + +####################################################### +# Tmux Terminal Multiplexor Support +# Link: https://github.com/tmux/tmux/wiki +####################################################### + +# Aliases for tmux terminal multiplexer if installed +if hascommand tmux; then + + # Enhance clear to also clear the TMUX scrollback buffer + if [[ -n "${TMUX}" ]]; then + alias clear='clear && tmux clear-history' + fi + + # If you connect to a session with a different resolution terminal + # this will freeze the size of the Tmux session window and not auto-resize + #tmux resize-window -A &> /dev/null + + # Load TMUX with default session defined by _TMUX_LOAD_SESSION_NAME + # If TMUX is already running, switch to a session name passed in as a parameter + # TIP: Use CTRL+d to detach your session which closes but leaves the session + # running. CTRL+d will also exit bash once outside of TMUX. + # alias tm='tmux a -t main || tmux new -s main' + function tm() { + + # Get the passed in or default session name + if [[ -n "${@}" ]]; then + local SESSION_NAME="${@}" + elif [[ -n "${_TMUX_LOAD_SESSION_NAME}" ]]; then + local SESSION_NAME="${_TMUX_LOAD_SESSION_NAME}" + elif [[ "$(tmux list-sessions 2> /dev/null | wc -l)" -gt 0 ]]; then + local SESSION_NAME="$(tmux ls -F "#{session_name}" | createmenu)" + else + local SESSION_NAME="$(whoami)" + fi + + # Create the session if it doesn't exist + TMUX='' tmux -u new-session -d -s "${SESSION_NAME}" 2> /dev/null + + # Attach if outside of TMUX + if [[ -z "$TMUX" ]]; then + tmux -u attach -t "${SESSION_NAME}" 2> /dev/null && exit + + # Switch if we are already inside of TMUX + else + tmux -u switch-client -t "${SESSION_NAME}" 2> /dev/null + fi + } + + function tmsessiongroup() { + # Tmux allows you to create "session groups" - multiple sessions that can all attach to the same set of windows + # (Allow multiple monitors to attach to the same session but independently view separate panes) + # Link: https://unix.stackexchange.com/questions/282365/using-multiple-terminal-x-windows-with-one-tmux-session + # To switch from one to the other using xdotool: + # xdotool search --name 'session1:0:' windowactivate + # xdotool search --name 'session2:1:' windowactivate + + # Get a list of sessions + local _TMUX_OPEN_SESSIONS="$(tmux ls -F "#{session_name}" 2> /dev/null)" + + # If the chosen session is blank + if [ -z "${_TMUX_OPEN_SESSIONS}" ]; then + # Show an error and exit + echo -e "${BRIGHT_RED}ERROR: ${BRIGHT_CYAN}No session found${RESET}" + return 1 + fi + + # If a new session name was not passed in... + if [ $# -eq 0 ]; then + # Get the name of the last session + local _TMUX_LAST_SESSION=$(echo "${_TMUX_OPEN_SESSIONS}" | head -n 1) + + # Generate a new session name based on the last session + local _COUNTER=2 + local _TMUX_RANDOM_NAME + while [ -z "${_TMUX_RANDOM_NAME}" ]; do + if echo "${_TMUX_OPEN_SESSIONS}" | grep -qxF "${_TMUX_LAST_SESSION}${_COUNTER}"; then + let _COUNTER=_COUNTER+1 + else + _TMUX_RANDOM_NAME="${_TMUX_LAST_SESSION}${_COUNTER}" + fi + done + + # Ask for the new session name from a menu + read -e -i "${_TMUX_RANDOM_NAME}" -p "New Session Name: " _TMUX_NEW_SESSION + else + # Use the passed in session name + local _TMUX_NEW_SESSION="${@}" + fi + + # Make sure the new session name doesn't already exist + if echo "${_TMUX_OPEN_SESSIONS}" | grep -qxF "${_TMUX_NEW_SESSION}"; then + # Show an error and exit + echo -e "${BRIGHT_RED}ERROR: ${BRIGHT_CYAN}Session ${BRIGHT_YELLOW}${_TMUX_NEW_SESSION}${BRIGHT_CYAN} already exists${RESET}" + return 1 + fi + + # If the chosen session is blank + if [[ -z "${_TMUX_NEW_SESSION}" ]]; then + # Show an error and exit + echo -e "${BRIGHT_RED}ERROR: ${BRIGHT_CYAN}You must specify a new session name${RESET}" + return 1 + fi + + # Change the prompt + local _PS3_OLD="${PS3}" + PS3="Choose an existing session to connect to: " + + # Ask for the session to connect to + local _TMUX_EXISTING_SESSION=$(echo "${_TMUX_OPEN_SESSIONS}" | sort | createmenu) + + # Put the prompt back to its original value + PS3="${_PS3_OLD}" + + # If the chosen session is blank + if [ -z "${_TMUX_EXISTING_SESSION}" ]; then + # Show an error and exit + echo -e "${BRIGHT_RED}ERROR: ${BRIGHT_CYAN}You must specify an existing session${RESET}" + return 1 + fi + + # Create a session group + if tmux -u new-session -t "${_TMUX_EXISTING_SESSION}" -s "${_TMUX_NEW_SESSION}"; then + return 0 + else + # Show an error and exit + echo -e "${BRIGHT_RED}ERROR: ${BRIGHT_CYAN}Could not connect to session ${BRIGHT_YELLOW}${_TMUX_EXISTING_SESSION}${RESET}" + return 1 + fi + } + + # Detach from Tmux - and optionally run a command if specified + # For example, detach and log into an SSH server already running Tmux + # so you don't run a Tmux session inside of your local Tmux session + # Example: tmd ssh username@server.net + function tmd() { + # If Tmux is running... + if [[ -n "$TMUX" ]]; then + # If no parameters were pass in... + if [ $# -eq 0 ]; then + # Detach from the session + tmux detach + else + # Detach and then execute the parameters + tmux detach -E "$*" + fi + + # Tmux is NOT running so no need to detach... + else + # Only do something if a command is passed in... + if [ $# -gt 0 ]; then + # Run the command + eval "${@}" + fi + fi + } + + # List and choose a buffer (clipboard history) + alias {tmb,tmc}='tmux choose-buffer' + + # List all the sessions + alias tmlist='tmux ls' + + # List all the connected clients + alias tmclients='tmux lsc' + + # Create a new session + alias {tmnew,tmjoin}='tm' + + # Attach to an existing session + #alias tmattach='tmux -u new-session -A -s' + alias tmattach='tmux -u attach-session -t "$(tmux ls -F "#{session_name}" | createmenu)"' + + # Rename a session: tmrename [session] [new_name] + alias tmrename='tmux rename -t' + + # Kill a session + alias tmkill='tmux kill-session -t' + + # Kill all sessions + alias tmreset='tmux kill-server' + + # List all the bound keys + alias tmlistkeys='grep -E "^(bind-key|bind)\s+" ~/.tmux.conf | sort' + +# Tmux is not installed, but Zellij is and is very user friendly +elif hascommand zellij; then + # Redirect these Tmux aliases to Zellij instead + alias {tm,tmnew,tmjoin}='znew' + alias tmlist='zlist' + alias tmattach='zattach' + alias tmkill='zkill' + alias tmreset='zreset' + +# Tmux is not installed, but Screen is +# Link: https://linuxize.com/post/how-to-use-linux-screen/ +elif hascommand screen; then + alias tm="echo 'Tmux is not installed, but screen is installed on this system.'" + +fi + +####################################################### +# Zellij Terminal Multiplexor Support +# Link: https://zellij.dev/documentation/introduction.html +####################################################### + +# Aliases for Zellij terminal multiplexer if installed +if hascommand zellij; then + + # Load Zellij with a default session named the logged in user + # You can also pass in a specific session name as a parameter + function znew() { + # Get the passed in or default session name + if [[ -n "${@}" ]]; then + local SESSION_NAME="${@}" + elif [[ "$(zellij list-sessions 2> /dev/null | wc -l)" -gt 0 ]]; then + local SESSION_NAME="" + else + local SESSION_NAME="$(whoami)" + fi + + # Create the session if it doesn't exist + zellij --session "${SESSION_NAME}" 2> /dev/null || zellij attach "${SESSION_NAME}" + } + + # Aliases for Zellij + alias {zj,zjoin}='znew' + alias zlist='zellij list-sessions' + alias zattach='zellij attach "$(zellij list-sessions | createmenu)"' + alias zkill='zellij kill-session' + alias zreset='zellij kill-all-sessions' + + # Include the bash completion and aliases from Zellij (i.e. zr, zrf, ze, zef) + builtin source <(zellij setup --generate-completion bash) >/dev/null 2>&1 +fi + +####################################################### +# Abduco lets programs run independently of the controlling terminal +# Note: CTRL+\ will detach the session +# Link: https://www.brain-dump.org/projects/abduco/ +####################################################### + +if hascommand abduco; then + + # Start or connect to an abduco session + function aa() { + if [[ ${#} -eq 0 ]]; then + if [[ $(abduco | wc -l) -gt 1 ]]; then + abduco -a "$(abduco | tail -n +2 | sed 's:\s\+:\t:g' | cut -f 5 | createmenu)" bash + else + abduco -c "${USER}" bash + fi + else + abduco -A "${@}" bash + fi + } + + # Connect to an abduco session read-only + function aaro() { + abduco -Ar "${@}" bash + } + + # List any abduco sessions + alias aals='abduco' + + # Kill all abduco sessions + alias aareset='pkill abduco' +fi + +####################################################### +# Easy Cross-Platform Package Management Aliases +####################################################### + +# Depending on the installed package managers, set up some package aliases +if hascommand --strict paru; then # Arch + # Link: https://github.com/Morganamilo/paru + # Link: https://itsfoss.com/paru-aur-helper/ + # NOTE: To get search results to start at the bottom and go upwards, enable BottomUp in paru.conf + alias has='paru -Si' + alias pkgupdateall='paru -Syyu --sudoloop --noconfirm --newsonupgrade && if type flatpak >/dev/null 2>&1; then flatpak update --appstream && flatpak update --assumeyes --noninteractive; fi && if type snap >/dev/null 2>&1; then sudo snap refresh; fi && if type tldr >/dev/null 2>&1; then tldr --update; fi' + alias pkgupdate='paru --sync --sudoloop --noconfirm' + alias pkginstall='paru --sync --sudoloop --noconfirm' + alias pkgremove='paru --remove' + alias pkgclean='paru --clean' + alias pkgsearch='paru --bottomup' + alias pkglist='paru -Qe' + alias pkglistmore='paru -Q' # Also includes dependencies + alias aurcheck='paru -Qua' # | sort | command less --no-init --ignore-case --LONG-PROMPT --LINE-NUMBERS' +elif hascommand --strict yay; then # Arch + # Link: https://github.com/Jguer/yay + alias has='yay -Si' + alias pkgupdateall='yay -Syyu --sudoloop && if type flatpak >/dev/null 2>&1; then flatpak update --appstream && flatpak update; fi && if type snap >/dev/null 2>&1; then sudo snap refresh; fi && if type tldr >/dev/null 2>&1; then tldr --update; fi' + alias pkgupdate='yay -S' + alias pkginstall='yay -S' + alias pkgremove='yay -Rns' + alias pkgclean='yay -Yc' + alias pkgsearch='yay' + alias pkglist='yay -Qe' + alias pkglistmore='yay -Q' # Also includes dependencies + alias aurcheck='yay -Qua' # | sort | command less --no-init --ignore-case --LONG-PROMPT --LINE-NUMBERS' +elif hascommand --strict pamac; then # Manjaro + # Link: https://wiki.manjaro.org/index.php/Pamac + alias has='sudo pamac info' + alias pkgupdateall='sudo pamac upgrade -a && if type tldr >/dev/null 2>&1; then tldr --update; fi' + alias pkgupdate='sudo pamac update' + alias pkginstall='sudo pamac install' + alias pkgremove='sudo pamac remove' + alias pkgclean='sudo pamac remove --orphans' + alias pkgsearch='sudo pamac search -a' + alias pkglist='pacman -Qe' + alias pkglistmore='pacman -Q' # Also includes dependencies +elif hascommand --strict pacman && [[ -f /etc/arch-release ]]; then # Arch (No AUR) + # Link: https://archlinux.org/pacman/ + alias has='pacman -Q --info' + alias pkgupdateall='sudo pacman -Syyu && if type flatpak >/dev/null 2>&1; then flatpak update --appstream && flatpak update; fi && if type snap >/dev/null 2>&1; then sudo snap refresh; fi && if type tldr >/dev/null 2>&1; then tldr --update; fi' + alias pkgupdate='sudo pacman -S' + alias pkginstall='sudo pacman -S' + alias pkgremove='sudo pacman -Rns' + alias pkgclean='pacman -Qtdq | sudo pacman -Rns -' + alias pkgsearch='pacman -Ss' + alias pkglist='pacman -Qe' + alias pkglistmore='pacman -Q' # Also includes dependencies +elif hascommand --strict dnf; then # RedHat/Fedora + # Link: https://fedoraproject.org/wiki/DNF + alias has='dnf info' + alias pkgupdateall='sudo dnf upgrade --refresh' + alias pkgupdate='sudo dnf upgrade' + alias pkginstall='sudo dnf install' + alias pkgremove='sudo dnf remove' + alias pkgclean='sudo dnf autoremove' + alias pkgsearch='sudo dnf search' + alias pkglist='dnf list installed' + alias pkgdependencies='yum whatprovides' +elif hascommand --strict yum; then # RedHat/Fedora + # Link: https://access.redhat.com/articles/yum-cheat-sheet + alias has='yum info' + alias pkgupdateall='sudo yum clean all && yum -y update' + alias pkgupdate='sudo yum update' + alias pkginstall='sudo yum install' + alias pkgremove='sudo yum remove' + alias pkgclean='sudo yum autoremove' + alias pkgsearch='sudo yum search' + alias pkglist='yum list installed' + alias pkgdependencies='yum whatprovides' +elif hascommand --strict nala; then # Debian/Ubuntu/Raspbian + # Link: https://gitlab.com/volian/nala + # Link: https://itsfoss.com/nala/ + alias has='nala show' + alias pkgupdateall='sudo nala update && sudo nala upgrade && if type pacstall >/dev/null 2>&1; then pacstall --upgrade; fi' + alias pkgupdate='sudo nala update' + alias pkginstall='sudo nala install --install-suggests' + alias pkgremove='sudo nala remove' + alias pkgclean='sudo nala clean --fix-broken' + alias pkgsearch='sudo nala search' + alias pkglist='sudo nala list --installed' + alias pkgmirrors='sudo nala fetch' +elif hascommand --strict apt; then # Debian/Ubuntu/Raspbian + # Link: https://itsfoss.com/apt-command-guide/ + alias has='apt show' + alias pkgupdateall='sudo apt update --assume-yes && sudo apt upgrade --assume-yes && if type pacstall >/dev/null 2>&1; then pacstall --upgrade; fi && if type tldr >/dev/null 2>&1; then tldr --update; fi' + alias pkgupdate='sudo apt-get install --only-upgrade' + alias pkginstall='sudo apt install' + alias pkgremove='sudo apt remove' + alias pkgclean='sudo apt autoremove' + alias pkgsearch='sudo apt search' + alias pkglist='sudo apt list --installed' + alias pkgcheck='sudo apt update --assume-yes && apt list --upgradable' +elif hascommand --strict apt-get; then # Debian/Ubuntu + # Link: https://help.ubuntu.com/community/AptGet/Howto + alias has='apt-cache show' + alias pkgupdateall='sudo apt-get update && sudo apt-get upgrade && if type pacstall >/dev/null 2>&1; then pacstall --upgrade; fi && if type tldr >/dev/null 2>&1; then tldr --update; fi' + alias pkgupdate='sudo apt-get install --only-upgrade' + alias pkginstall='sudo apt-get install' + alias pkgremove='sudo apt-get remove' + alias pkgclean='sudo apt-get autoremove' + alias pkgsearch='sudo apt-cache search' + alias pkglist='sudo dpkg -l' +elif hascommand --strict zypper; then # SUSE + # Link: https://en.opensuse.org/SDB:Zypper_usage + alias has='zypper info' + alias pkgupdateall='sudo zypper patch' + alias pkgupdate='sudo zypper up' + alias pkginstall='sudo zypper in' + alias pkgremove='sudo zypper rm' + alias pkgclean='sudo zypper packages --orphaned' + alias pkgsearch='sudo zypper se' + alias pkglist='zypper se --installed-only' +elif hascommand --strict eopkg; then # Solus + # Link: https://getsol.us/articles/package-management/basics/en/ + alias has='sudo eopkg info' + alias pkgupdateall='sudo eopkg upgrade' + alias pkgupdate='sudo eopkg upgrade' + alias pkginstall='sudo eopkg install' + alias pkgremove='sudo eopkg remove' + alias pkgsearch='sudo eopkg search' + alias pkglist='eopkg li -l' +elif hascommand --strict emerge; then # Gentoo (Portage) + # Link: https://www.linode.com/docs/guides/portage-package-manager/ + alias has='equery files' + alias pkgupdateall='sudo emerge --sync && sudo emerge --update --deep --with-bdeps=y --newuse @world && sudo emerge --depclean && sudo revdep-rebuild' + alias pkgupdate='sudo emerge -u' + alias pkginstall='sudo emerge' + alias pkgremove='sudo emerge --depclean' + alias pkgsearch='sudo emerge --search' + alias pkglist='equery list "*"' +elif hascommand --strict slackpkg; then # Slackware + # Link: https://www.linux.com/training-tutorials/intro-slackware-package-management/ + alias has='slackpkg info' + alias pkgupdateall='slackpkg update && slackpkg install-new && slackpkg upgrade-all' + alias pkgupdate='upgradepkg' + alias pkginstall='installpkg' + alias pkgremove='removepkg' + alias pkglist='pkgtool' +elif hascommand --strict urpmi; then # Mandrake + # Link: https://wiki.mageia.org/en/URPMI + alias has='urpmq --summary -Y' + alias pkgupdateall='urpmi --auto-update' + alias pkgupdate='urpmi' + alias pkginstall='urpmi' + alias pkgremove='urpme' + alias pkgsearch='urpmq --summary -Y' + alias pkglist='rpm -qa' +elif hascommand --strict apt-cyg; then # Cygwin + # Link: http://stephenjungels.com/jungels.net/projects/apt-cyg/ + alias has='apt-cyg show' + alias pkgupdateall='apt-cyg update' + alias pkgupdate='apt-cyg update' + alias pkginstall='apt-cyg install' + alias pkgremove='apt-cyg remove' + alias pkgsearch='apt-cyg find' + alias pkglist='cygcheck --check-setup' +elif hascommand --strict brew; then # macOS + # Link: https://brew.sh/ + alias has='brew info' + alias pkgupdateall='brew update' + alias pkgupdate='brew update' + alias pkginstall='brew install' + alias pkgremove='brew uninstall' + alias pkgclean='brew cleanup' + alias pkgsearch='brew search' + alias pkglist='brew list' +fi + +# If this is an Arch based distrobution with pacman... +if hascommand --strict pacman && [[ -d /etc/pacman.d/ ]]; then + + # Install a list of packages with regex + # https://wiki.archlinux.org/title/pacman#Installing_specific_packages + function pkginstallregex() { + if [ $# -eq 0 ]; then + echo "No regex provided" + return 1 + else + sudo pacman -S $(pacman -Ssq "${@}") + fi + } + + # Clean the pacman and helper package caches + alias pacman-clean-cache='sudo echo -ne "${BRIGHT_YELLOW}Before:${RESET} "; sudo du -sh /var/cache/pacman/pkg/ 2>/dev/null; yes | sudo pacman -Scc && command -v yay &> /dev/null && yes | yay -Sc || true && command -v paru &> /dev/null && yes | paru -Sc || true; echo -ne "\n${BRIGHT_GREEN}After:${RESET} "; sudo du -sh /var/cache/pacman/pkg/ 2>/dev/null' + + # To mark a package as explicitly installed or only a dependency + alias pkgmarkasexplicit='sudo pacman -D --asexplicit' + alias pkgmarkasdependency='sudo pacman -D --asdeps' + + # Show all packages and their install reason + alias pkgreasons="pacman -Qi | awk -F': ' '/^Name/ { name = \$2; } /^Install Reason/ { reason = \$2; } /^$/ { printf \"%s: %s\\n\", name, reason; }'" + + # Check for default configuration file default backups + alias pacnew='sudo true && echo "Pacman backup configuration files found:"; sudo find /etc -type f \( -iname \*.pacnew -o -iname \*.pacsave \) | sort -t"/" -k2.2r -k2.1' + + # Force remove a package ignoring required dependencies + # NOTE: Also can be typed as sudo pacman -Rdd for short + alias pkgforceremove='sudo pacman -Rd --nodeps' + + # Force remove a package ignoring required dependencies and then reinstall + function pkgforcereinstall() { + if [[ -z "${1}" ]]; then + echo -e "${BRIGHT_RED}Error:${RESET} Package name required" + return 1 + fi + sudo pacman -Rd --nodeps "${1}" && sudo pacman -S "${1}" + } + + # Search for a package containing a file + alias pkgsearchcontainingfile='sudo pacman -Fy' + + # List all the local files in an installed package + alias pkglocalpackagefiles='pacman -Ql' + + # Verify the presence of the files installed by a package + alias pkgverifylocalpackage='sudo pacman -Qkk' + + # Verify all packages + # Link: https://unix.stackexchange.com/questions/659756/arch-linux-reinstall-all-broken-packages-after-poweroff-during-system-upgrade + alias pkgverifyall="pacman -Qk 2>/dev/null | grep -v ' 0 missing files'" + + # Show the latest Arch linux update news + alias archnews='w3m https://www.archlinux.org/ | sed -n "/Latest News/,/Older News/p" | "${PAGER}"' + + # Pacseek - browse and search through the Arch Linux package databases and AUR + # Link: https://github.com/moson-mo/pacseek + if hascommand --strict pacseek; then + alias pkg='pacseek' + fi + + # If perl is installed (preset on most systems)... + # Link: https://wiki.archlinux.org/title/Perl + # Install: sudo pacman -S perl + if hascommand --strict perl; then + + # Use a much more detailed package listing with descriptions (AUR separated) + # pkglist [search] will search for all installed packages instead + # Perl pipeline: collapse padding, wrap groups in parens, then slurp-mode + # regex reduces each package block to: Name (Group) - Description + alias pkglist &>/dev/null && unalias pkglist + function pkglist() { + if [ $# -eq 0 ]; then + # Native Arch packages (from official repos) + # -Q = query local, -e = explicit, -n = native (repo), -i = info + echo -e "${BRIGHT_BLUE}=============== ${BRIGHT_YELLOW}Native Arch Packages${BRIGHT_BLUE} ===============${RESET}" + pacman -Qeni \ + | perl -pe 's/ +/ /gm' \ + | perl -pe 's/^(Groups +: )(.*)/\1(\2)/gm' \ + | perl -0777 -pe 's/^Name : (.*)\nVersion :(.*)\nDescription : ((?!None).*)?(?:.|\n)*?Groups :((?! \(None\)$)( )?.*)?(?:.|\n(?!Name))+/$1$4 - $3/gm' \ + | grep -A1 --color -P '^[^\s]+' + + # AUR / Chaotic-AUR / custom repository packages (not in official repos) + # -m = foreign (not found in any configured repo) + echo -e "\n${BRIGHT_BLUE}=============== ${BRIGHT_YELLOW}Arch User Repository (AUR)${BRIGHT_BLUE} ===============${RESET}" + pacman -Qemi \ + | perl -pe 's/ +/ /gm' \ + | perl -pe 's/^(Groups +: )(.*)/\1(\2)/gm' \ + | perl -0777 -pe 's/^Name : (.*)\nVersion :(.*)\nDescription : ((?!None).*)?(?:.|\n)*?Groups :((?! \(None\)$)( )?.*)?(?:.|\n(?!Name))+/$1$4 - $3/gm' \ + | grep -A1 --color -P '^[^\s]+' + else + # If a search parameter was specified, grep all explicit packages + pacman -Qei \ + | perl -pe 's/ +/ /gm' \ + | perl -pe 's/^(Groups +: )(.*)/\1(\2)/gm' \ + | perl -0777 -pe 's/^Name : (.*)\nVersion :(.*)\nDescription : ((?!None).*)?(?:.|\n)*?Groups :((?! \(None\)$)( )?.*)?(?:.|\n(?!Name))+/$1$4 - $3/gm' \ + | grep -A1 --color -P '^[^\s]+' \ + | grep -i "${@}" + fi + } + fi + + # If pacman-contrib (contributed scripts and tools for pacman) is installed... + # Install: sudo pacman -S pacman-contrib + if hascommand --strict paccache; then + + # Add aliases to find dependencies + alias pkgdependencies='pactree --color' # --unique --depth 1 + alias pkgwhatuses='pactree --reverse --color' + + # The checkupdates script (also from Arch pacman-contrib) + # NOTE: The benefit to this is it does NOT need SUDO/ROOT access + alias pkgcheck='checkupdates | sort | command less --no-init --ignore-case --LONG-PROMPT --LINE-NUMBERS' + + # Alias to fix Arch Pacman install error "invalid or corrupted package" with a + # new PGP key, clear anything older than the last 3 installs, and remove locks + # Link: https://odysee.com/@DistroTube:2/solved-pacman-wouldn't-let-me-run-an:0 + alias pacmanfix='sudo rm -f /var/lib/pacman/db.lck && sudo paccache -r && sudo pacman -Sy archlinux-keyring' + alias pacmanfixkeys='sudo rm -f /var/lib/pacman/db.lck; sudo rm -R /etc/pacman.d/gnupg/; sudo rm -R /root/.gnupg/; sudo gpg --refresh-keys && sudo pacman-key --init && sudo pacman-key --populate && sudo pacman -Sy archlinux-keyring' + fi +fi + +# If apt package manager is installed, add aliases to find dependencies in Ubuntu/Debian +if hascommand --strict apt-cache; then + alias pkgdependencies='apt-cache depends' + # If apt-rdepends is installed (sudo apt install apt-rdepends) + if hascommand --strict apt-rdepends; then + alias pkgwhatuses='apt-rdepends' + fi +fi + +# Aliases for flatpak packages if installed +if hascommand --strict flatpak; then + alias flatpakhas='flatpak info' + alias flatpakcheck='flatpak update --appstream && flatpak remote-ls --updates' + alias flatpakupdate='flatpak update --appstream && flatpak update' + alias flatpakinstall='flatpak install' + alias flatpakremove='flatpak uninstall' + alias flatpakwipe='flatpak uninstall --delete-data' + alias flatpakclean='flatpak repair && flatpak uninstall --unused' + alias flatpaksearch='flatpak search' + alias flatpaklist='flatpak list --columns=name,app:f,version:e,description:e --app' + alias flatpaksize='flatpak list --columns=app:f,name,size:f,installation:e' + alias flatpakremotes='flatpak remotes --show-details' + + # Create missing or recreate broken Flatpak icons (might require restart) + if [ -d "/var/lib/flatpak/exports/share/applications/" ]; then + alias flatpakmakeicons='command cp /var/lib/flatpak/exports/share/applications/*.desktop ~/.local/share/applications/' + else + alias flatpakmakeicons='find /var/lib/flatpak/app/ -type f -iname "*.desktop" -exec cp {} ~/.local/share/applications \;' + fi + + # Create an executable backup script for Flatpak apps and custom permissions + flatpakbackup() { + # Require sudo rights unless we're only showing help + if [[ ! "${1}" =~ ^(-h|--help)$ ]] && (( EUID != 0 )); then + if ! sudo -v 2>/dev/null; then + echo -e "${BRIGHT_RED}Error:${RESET} root (sudo) privileges are needed to read system-level Flatpak overrides" + return 1 + fi + fi + + # Handle command line arguments + case "${1}" in + -h|--help) + echo -e "${BRIGHT_CYAN}flatpakbackup${RESET}: Generate bash commands to backup and restore Flatpak permissions" + echo -e "${BRIGHT_WHITE}Description:${RESET} Captures system/user global overrides and app-specific overrides" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}flatpakbackup${RESET} ${BRIGHT_MAGENTA}[${BRIGHT_GREEN}output_file${BRIGHT_MAGENTA}]${RESET}" + echo -e "${BRIGHT_WHITE}Examples:${RESET}" + echo -e " ${BRIGHT_CYAN}flatpakbackup${RESET} ${BRIGHT_BLUE}# Output to terminal${RESET}" + echo -e " ${BRIGHT_CYAN}flatpakbackup${RESET} ${BRIGHT_YELLOW}restore.sh${RESET} ${BRIGHT_BLUE}# Save to executable file${RESET}" + return 0 + ;; + "") + # Output to terminal + ;; + *) + # Cleanup orphaned app data + echo -e "${BRIGHT_CYAN}Clean up data from orphaned Flatpak apps? (${BRIGHT_GREEN}y${BRIGHT_CYAN}/${BRIGHT_RED}N${BRIGHT_CYAN}):${RESET} \c" + read -r cleanup_confirm + if [[ ${cleanup_confirm} =~ ^[yY]$ ]]; then + installed="$(flatpak list --app --columns=application 2>/dev/null | LC_ALL=C sort -fu)" + for dir in "${HOME}/.var/app"/*/; do + [[ -d "${dir}" ]] || continue + app_id="$(basename "${dir}")" + if ! grep -qxF "${app_id}" <<< "${installed}"; then + echo -e "\n${BRIGHT_MAGENTA}Orphaned:${RESET} ${BRIGHT_WHITE}${app_id}${RESET}" + echo -e "${BRIGHT_CYAN}Remove ${BRIGHT_YELLOW}${dir}${BRIGHT_CYAN}? (${BRIGHT_GREEN}y${BRIGHT_CYAN}/${BRIGHT_RED}N${BRIGHT_CYAN}):${RESET} \c" + read -r confirm + if [[ ${confirm} =~ ^[yY]$ ]]; then + rm -rf -- "${dir}" + echo -e "${BRIGHT_GREEN}✓ Removed${RESET}" + else + echo -e "${BRIGHT_RED}X Skipped${RESET}" + fi + fi + done + fi + + # Backup app data (use strongest widely-available compressor) + echo -e "\n${BRIGHT_CYAN}Create compressed backup of ${BRIGHT_YELLOW}~/.var/app/${BRIGHT_CYAN} (all Flatpak app data)? (${BRIGHT_GREEN}y${BRIGHT_CYAN}/${BRIGHT_RED}N${BRIGHT_CYAN}):${RESET} \c" + read -r backup_confirm + if [[ ${backup_confirm} =~ ^[yY]$ ]]; then + # Build a relative tar (no absolute paths) and choose best available compression. + if command -v xz >/dev/null 2>&1; then + outfile="flatpak-app-data-$(date +%Y%m%d-%H%M%S).tar.xz" + tar -C "${HOME}/.var" -I "xz -T0 -9e" -cf "${outfile}" app + elif command -v zstd >/dev/null 2>&1; then + outfile="flatpak-app-data-$(date +%Y%m%d-%H%M%S).tar.zst" + tar -C "${HOME}/.var" -I "zstd -T0 -19" -cf "${outfile}" app + else + outfile="flatpak-app-data-$(date +%Y%m%d-%H%M%S).tar.gz" + tar -C "${HOME}/.var" -I "gzip -9" -cf "${outfile}" app + fi + echo -e "${BRIGHT_GREEN}✓ Backup created:${RESET} ${BRIGHT_YELLOW}${outfile}${RESET}" + fi + + # Save to file - redirect output and make executable + flatpakbackup "" > "${1}" + chmod ug+x "${1}" + echo -e "${BRIGHT_GREEN}✓ Flatpak backup saved to:${RESET} ${BRIGHT_YELLOW}${1}${RESET}" + return 0 + ;; + esac + + # Helper function to parse individual override lines into flatpak commands + _parse_override_line() { + local LINE="$1" + local COMMAND_PREFIX="$2" # e.g., "sudo flatpak override --system" + local COMMAND_SUFFIX="$3" # e.g., ' "com.example.App"' or "" + + # Skip section headers like [Context], [Environment], etc. + if [[ "${LINE}" =~ ^\[.*\]$ ]]; then + return 0 + fi + + case "${LINE}" in + # Explicit env unsets as reported by --show + "unset-env="*) + local VALUES="${LINE#*=}" + IFS=';' read -ra ITEMS <<< "${VALUES}" + for ITEM in "${ITEMS[@]}"; do + [[ -n "${ITEM}" ]] && echo "${COMMAND_PREFIX} --unset-env=${ITEM}${COMMAND_SUFFIX}" + done + ;; + + # Regular permissions (positive/negative items separated by ;) + "filesystems="*|"sockets="*|"devices="*|"shared="*|"own-name="*|"talk-name="*|"persist="*|"features="*) + local KEY="${LINE%%=*}" + local VALUES="${LINE#*=}" + local OPTION_MAP="" + case "${KEY}" in + "filesystems") OPTION_MAP="--filesystem" ;; + "sockets") OPTION_MAP="--socket" ;; + "devices") OPTION_MAP="--device" ;; + "shared") OPTION_MAP="--share" ;; + "own-name") OPTION_MAP="--own-name" ;; + "talk-name") OPTION_MAP="--talk-name" ;; + "persist") OPTION_MAP="--persist" ;; + "features") OPTION_MAP="--feature" ;; + esac + + IFS=';' read -ra ITEMS <<< "${VALUES}" + for ITEM in "${ITEMS[@]}"; do + [[ -z "${ITEM}" ]] && continue + if [[ "${ITEM}" == "!"* ]]; then + # Negations map to specific --no*/--unshare flags + local VALUE="${ITEM#!}" + case "${KEY}" in + "sockets") echo "${COMMAND_PREFIX} --nosocket=${VALUE}${COMMAND_SUFFIX}" ;; + "filesystems") echo "${COMMAND_PREFIX} --nofilesystem=${VALUE}${COMMAND_SUFFIX}" ;; + "devices") echo "${COMMAND_PREFIX} --nodevice=${VALUE}${COMMAND_SUFFIX}" ;; + "shared") echo "${COMMAND_PREFIX} --unshare=${VALUE}${COMMAND_SUFFIX}" ;; + "own-name") echo "${COMMAND_PREFIX} --no-own-name=${VALUE}${COMMAND_SUFFIX}" ;; + "talk-name") echo "${COMMAND_PREFIX} --no-talk-name=${VALUE}${COMMAND_SUFFIX}" ;; + "persist") echo "${COMMAND_PREFIX} --no-persist=${VALUE}${COMMAND_SUFFIX}" ;; + "features") echo "${COMMAND_PREFIX} --no-feature=${VALUE}${COMMAND_SUFFIX}" ;; + *) echo "# TODO: unknown negation for key=${KEY} item=${VALUE}" ;; + esac + else + echo "${COMMAND_PREFIX} ${OPTION_MAP}=${ITEM}${COMMAND_SUFFIX}" + fi + done + ;; + + # Fallback: treat bare KEY=VALUE from [Environment] as --env + *"="*) + # Avoid re-catching known keys above + if [[ "${LINE}" != filesystems=* && "${LINE}" != sockets=* && "${LINE}" != devices=* && \ + "${LINE}" != shared=* && "${LINE}" != own-name=* && "${LINE}" != talk-name=* && \ + "${LINE}" != persist=* && "${LINE}" != features=* && "${LINE}" != unset-env=* ]] + then + echo "${COMMAND_PREFIX} --env=${LINE}${COMMAND_SUFFIX}" + fi + ;; + esac + } + + # Generate the restore script header + echo "#!/usr/bin/env bash" + echo "set -euo pipefail # fail fast on errors / unset vars / pipeline failures" + echo "# Flatpak permissions restore script generated on $(date '+%B %d, %Y at %I:%M %p')" + echo "# This script will recreate all your Flatpak app installations and permission overrides" + echo "" + + # === SYSTEM GLOBAL OVERRIDES === + echo "# System global overrides (applies to all apps for all users)" + echo "# Uncomment the reset line below to clear existing overrides first:" + echo "# sudo flatpak override --system --reset" + + local system_output + system_output="$(flatpak override --system --show 2>/dev/null)" + if [[ -n "${system_output}" ]]; then + echo "${system_output}" | while IFS= read -r line; do + _parse_override_line "${line}" "sudo flatpak override --system" "" + done | LC_ALL=C sort -fu + else + echo "# No system global overrides found" + fi + echo "" + + # === USER GLOBAL OVERRIDES === + echo "# User global overrides (applies to all apps for current user)" + echo "# Uncomment the reset line below to clear existing overrides first:" + echo "# flatpak override --user --reset" + + local user_output + user_output="$(flatpak override --user --show 2>/dev/null)" + if [[ -n "${user_output}" ]]; then + echo "${user_output}" | while IFS= read -r line; do + _parse_override_line "${line}" "flatpak override --user" "" + done | LC_ALL=C sort -fu + else + echo "# No user global overrides found" + fi + echo "" + + # === APP-SPECIFIC OVERRIDES === + echo "# App-specific overrides (sorted by app name for easy reference)" + echo "# Each app section includes installation command and custom permissions" + echo + + # Get all apps sorted by display name, then extract app IDs + # Use process substitution (< <(...)) so the while-loop runs in the + # current shell — this makes `local` work correctly inside the loop + while read -r app_id; do + # Skip header line and empty lines + if [[ -n "${app_id}" && "${app_id}" != "Application" ]]; then + # Get override settings for this app (both system and user) + local system_app_output user_app_output + system_app_output="$(flatpak override --system --show "${app_id}" 2>/dev/null)" + user_app_output="$(flatpak override --user --show "${app_id}" 2>/dev/null)" + + # Process all apps (even those without custom overrides) + # Get app description for the header comment + local app_description + app_description="$(printf '%s\n' "$(flatpak info "${app_id}" 2>/dev/null)" | awk 'NF { print; exit }')" + + # Create app section header + if [[ -n "${app_description}" ]]; then + echo "# $(printf '%.0s=' {1..76})" + echo "# ${app_description}" + echo "# $(printf '%.0s=' {1..76})" + fi + + # App installation and reset commands + # Detect current installation scope so restores match original location + local install_scope + install_scope="$(flatpak list --app --columns=installation,application 2>/dev/null \ + | awk -v id="${app_id}" 'BEGIN{FS="\t"} $2==id{print $1; exit}')" + + if [[ "${install_scope}" == "user" ]]; then + echo "flatpak install -y --or-update --noninteractive --user \"${app_id}\"" + else + echo "flatpak install -y --or-update --noninteractive --system \"${app_id}\"" + fi + + # Only add override sections if there are actual overrides + if [[ -n "${system_app_output}" || -n "${user_app_output}" ]]; then + echo "# Uncomment the reset lines below to clear existing app overrides first:" + echo "# sudo flatpak override --system --reset \"${app_id}\"" + echo "# flatpak override --user --reset \"${app_id}\"" + + # System-level app overrides + if [[ -n "${system_app_output}" ]]; then + echo "${system_app_output}" | while IFS= read -r line; do + _parse_override_line "${line}" "sudo flatpak override --system" " \"${app_id}\"" + done | LC_ALL=C sort -fu + fi + + # User-level app overrides + if [[ -n "${user_app_output}" ]]; then + echo "${user_app_output}" | while IFS= read -r line; do + _parse_override_line "${line}" "flatpak override --user" " \"${app_id}\"" + done | LC_ALL=C sort -fu + fi + fi + echo "" + fi + done < <(flatpak list --app --columns=name,application 2>/dev/null \ + | LC_ALL=C sort -fu \ + | cut -f2) + } + + # Make sure the Flatpak paths are present once (for icons/desktop files) + # Only append Flatpak paths if not already present (prevents duplicates on re-source) + if [[ ":${XDG_DATA_DIRS}:" != *":/var/lib/flatpak/exports/share:"* ]]; then + export XDG_DATA_DIRS="${XDG_DATA_DIRS:+${XDG_DATA_DIRS}:}/var/lib/flatpak/exports/share:${HOME}/.local/share/flatpak/exports/share" + fi +fi + +# Aliases for snap packages if installed +if hascommand --strict snap; then + alias snaphas='snap info' + alias snapcheck='snap refresh --list' + alias snapupdate='sudo snap refresh' + alias snapinstall='sudo snap install' + alias snapremove='sudo snap remove' + alias snapclean='LANG=C snap list --all | while read snapname ver rev trk pub notes; do if [[ $notes = *disabled* ]]; then sudo snap remove "$snapname" --revision="$rev"; fi; done' + alias snapsearch='snap find' + alias snaplist='snap list' + alias snapsize='echo "Snap package sizes:" && du -hcs /var/lib/snapd/snaps/* 2>/dev/null || echo "No snap packages found"' +fi + +####################################################### +# Alias for sudo replacements on machines with one user +####################################################### + +if [[ $_SKIP_SUDO_ALTERNATIVE = false ]]; then + + # A very slim alternative to both sudo and doas + # Link: https://codeberg.org/sw1tchbl4d3/rdo + # Config: sudoedit /etc/rdo.conf + # username=yourusername + # wrong_pw_sleep=1000 + # session_ttl=5 + if hascommand --strict rdo; then + alias sudo='rdo' + + # If sudoedit is not avaliable, alias it + if ! hascommand --strict sudoedit; then + # The edit alias might not exist in root but the + # edit function handles sudo editing when needed + alias sudoedit='edit' + fi + + # A port of OpenBSD's doas offers two benefits over sudo: + # 1) Its configuration file has a simple syntax and + # 2) It is smaller, requiring less effort to audit the code + # This makes it harder for both admins and coders to make mistakes that potentially open security holes in the system + # Link: https://github.com/Duncaen/OpenDoas or https://github.com/slicer69/doas + # Link: https://youtu.be/eamEZCj-CuQ + # Config: Add "permit as root" in /etc/doas.conf or /usr/local/etc/doas.conf + elif hascommand --strict doas; then + alias sudo='doas' + + # Replace sudoedit only if doasedit exists + # Link: https://github.com/AN3223/scripts/blob/master/doasedit + if hascommand --strict doasedit; then + alias sudoedit='doasedit' + fi + fi +fi + +####################################################### +# General Function "Aliases" +####################################################### + +# Run this function upon exit of the shell +function _exit() { + # Show who logged out + local COLOR="${BRIGHT_RED}" # Light Red + local HIGHLIGHT="${BRIGHT_BLUE}" # Light Blue + local NOCOLOR="${RESET}" + echo -e "${COLOR}User ${HIGHLIGHT}$(echo $USER)${COLOR} has logged out of ${HIGHLIGHT}$(echo $HOSTNAME)${COLOR}.${NOCOLOR}" +} +trap _exit EXIT + +# Calculator that uses bc or Bash's built-in arithmetic +# Example: = 5*5+2 +if hascommand bc; then + # Start calculator with math support + # echo 'if (scale == 0) scale=4' > ~/.config/bcrc + # Link: https://www.gnu.org/software/bc/manual/html_mono/bc.html + alias bc='bc --mathlib' + if [[ -f ~/.bcrc ]]; then + export BC_ENV_ARGS=~/.bcrc + elif [[ -f ~/.config/bcrc ]]; then + export BC_ENV_ARGS=~/.config/bcrc + fi + export BC_LINE_LENGTH=0 + + function =() { + bc <<< "${@}" + } +else + function =() { + local IFS=' ' + local _CALC="${*//p/+}" + _CALC="${_CALC//x/*}" + echo "$(($_CALC))" + } +fi + +# Cross-platform realpath equivalent for resolving symlinks to an absolute path +# Uses readlink -f on Linux and an alternative approach on macOS which lacks -f +function resolvesymlink() { + # Show help if no argument provided or help requested + if [[ -z "${1}" ]] || [[ "${1}" == "--help" ]] || [[ "${1}" == "-h" ]]; then + echo -e "${BRIGHT_CYAN}resolvesymlink${RESET}: Resolve a symlink to its absolute target path" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}resolvesymlink${RESET} ${BRIGHT_MAGENTA}<${BRIGHT_YELLOW}symlink_path${BRIGHT_MAGENTA}>${RESET}" + echo -e "${BRIGHT_WHITE}Examples:${RESET}" + echo -e " ${BRIGHT_CYAN}resolvesymlink${RESET} ${BRIGHT_YELLOW}/usr/bin/python${RESET}" + return 2 + fi + + # Check if the system is running macOS (Darwin) + if [[ "$(uname)" == "Darwin" ]]; then + + # Initialize the TARGET_FILE variable to the input file path + local TARGET_FILE="$1" + local FULL_PATH + + # Use a subshell to prevent changing the current working directory + ( + # Change to the directory containing the target file + cd "$(dirname "${TARGET_FILE}")" || return 1 + + # Get the base name of the target file (strip directory path) + TARGET_FILE=$(basename "${TARGET_FILE}") + + # Resolve any symlinks by following them iteratively + while [[ -L "${TARGET_FILE}" ]]; do + # Update TARGET_FILE with the link's actual destination + TARGET_FILE=$(readlink "${TARGET_FILE}") + # Change directory to where the symlink points + cd "$(dirname "${TARGET_FILE}")" || return 1 + # Update TARGET_FILE to just the file name again + TARGET_FILE=$(basename "${TARGET_FILE}") + done + + # Get the absolute path of the final resolved file + FULL_PATH="$(pwd -P)/${TARGET_FILE}" + + # Output the resolved absolute path + echo "${FULL_PATH}" + ) + else + # Use readlink -f for Linux systems + readlink -f "$1" + fi +} + +# Confirm/Ask a question - See 'killps' for example of use +# General-purpose function to ask Yes/No questions in Bash, +# either with or without a default answer. +# It keeps repeating the question until it gets a valid answer. +# Link: https://gist.github.com/davejamesmiller/1965569 +# Example Usage: +# if ask "Do you want to do such-and-such?"; then +# Default to Yes if the user presses enter without giving an answer: +# if ask "Do you want to do such-and-such?" Y; then +# Default to No if the user presses enter without giving an answer: +# if ask "Do you want to do such-and-such?" N; then +# Or if you prefer the shorter version: +# ask "Do you want to do such-and-such?" && said_yes +# ask "Do you want to do such-and-such?" || said_no +function ask() { + # Initialize local variables + local prompt default reply + + # Determine the prompt and default based on the second parameter + if [[ "${2:-}" = "Y" ]]; then + prompt='Y/n' + default='Y' + elif [[ "${2:-}" = "N" ]]; then + prompt='y/N' + default='N' + else + prompt='y/n' + default='' + fi + + # Loop until a valid answer is given + while true; do + # Ask the question (not using "read -p" as it uses stderr not stdout) + echo -ne "${1} [$prompt] " + + # Read the answer (use /dev/tty in case stdin is redirected from somewhere else) + read -r reply /dev/null | createmenu)" +# Example: createmenu 'Option 1' 'Option 2' 'Option 3' +function createmenu() { + # Valid pickers to detect and automatically used in order + local _VALID_PICKERS="${_PREFERRED_PICKER} fzy sk fzf peco percol pick icepick selecta sentaku zf dmenu rofi wofi" + + # Check if command line arguments are provided and if input is piped in + if [[ "${1}" == "--help" ]] || [[ "${1}" == "-h" ]] || { [[ "$#" -eq 0 ]] && [[ -t 0 ]]; }; then + echo -e "${BRIGHT_CYAN}createmenu${RESET}: Create a menu for selecting an item from a list" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}createmenu${RESET} ${BRIGHT_MAGENTA}[${BRIGHT_GREEN}--picker=name${BRIGHT_MAGENTA}]${RESET} ${BRIGHT_MAGENTA}[${BRIGHT_GREEN}items...${BRIGHT_MAGENTA}]${RESET}" + echo -e " command | ${BRIGHT_CYAN}createmenu${RESET} ${BRIGHT_MAGENTA}[${BRIGHT_GREEN}--picker=name${BRIGHT_MAGENTA}]${RESET}" + echo -e "${BRIGHT_WHITE}Pickers:${RESET} fzy, sk, fzf, peco, percol, pick, icepick, selecta, sentaku, zf, dmenu, rofi, wofi" + echo -e "${BRIGHT_WHITE}Examples:${RESET}" + echo -e " ${BRIGHT_CYAN}createmenu${RESET} ${BRIGHT_YELLOW}'Option 1' 'Option 2' 'Option 3'${RESET}" + echo -e " ${BRIGHT_YELLOW}ls -1 ~${RESET} | ${BRIGHT_CYAN}createmenu${RESET}" + echo -e " ${BRIGHT_YELLOW}cat 'items.txt'${RESET} | ${BRIGHT_CYAN}createmenu${RESET}" + echo -e " ${BRIGHT_CYAN}createmenu${RESET} ${BRIGHT_GREEN}--picker=rofi${RESET} ${BRIGHT_YELLOW}'A' 'B' 'C'${RESET}" + return 1 + fi + + # Check for --picker parameter and remove it from arguments + local _PICKER + local _FOUND_PICKER=false + local NEW_ARGS=() + for ARG in "$@"; do + if [[ "$ARG" == --picker=* ]]; then + _PICKER="${ARG#*=}" + if type "${_PICKER}" &>/dev/null; then + _FOUND_PICKER=true + else + echo -e "${BRIGHT_RED}Error: ${BRIGHT_CYAN}The picker ${BRIGHT_YELLOW}${_PICKER}${BRIGHT_CYAN} is not available or installed${RESET}" + return 1 + fi + else + NEW_ARGS+=("$ARG") + fi + done + set -- "${NEW_ARGS[@]}" + + # If no specific picker is provided or the picker is not valid... + if [ "$_FOUND_PICKER" == false ]; then + # Loop through the list and see if one of them is installed + for _PICKER in $_VALID_PICKERS; do + if type $_PICKER &>/dev/null; then + _FOUND_PICKER=true + break + fi + done + fi + + # Check if command line arguments are provided + if [ "$#" -gt 0 ]; then + local _INPUT="" + local _COUNT=0 + for arg in "$@"; do + # Increase count for each argument + ((_COUNT++)) + + # Add newline after each argument except the last + if [ $_COUNT -lt $# ]; then + _INPUT+="${arg}"$'\n' + else + _INPUT+="${arg}" + fi + done + else + # Get the piped in multiple lines of text + local _INPUT="$( /dev/null + fi +} + +# Aliases crontab with safety warning for -r option, confirming removal +alias crontab='_crontab_safe' +function _crontab_safe() { + # Check if the parameters contain the "-r" option + if [[ "$*" == *"-r"* ]]; then + # Display a warning message in bright red with a warning icon + echo -e "${BRIGHT_RED}WARNING: ${RESET}${BRIGHT_CYAN}You are attempting to remove your crontab. This action cannot be undone! ${BRIGHT_YELLOW}⚠️${RESET}" + + # Ask for user confirmation before proceeding + read -p "Are you sure you want to continue? (y/N) " confirm + + # Check if the user confirmed the operation + if [[ "$confirm" == [yY] || "$confirm" == [yY][eE][sS] ]]; then + # If confirmed, execute the actual crontab command with the provided parameters + command crontab "$@" + else + # If not confirmed, display a cancellation message + echo "Operation canceled." + fi + else + # If the "-r" option is not present, execute the regular crontab command with the provided parameters + command crontab "$@" + fi +} + +# Search process names to kill +# https://unix.stackexchange.com/questions/443472/alias-for-killing-all-processes-of-a-grep-hit +function smash() { + # Check if a process name is specified; if not, show help text + if [[ -z "${1}" ]] || [[ "${1}" == "--help" ]] || [[ "${1}" == "-h" ]]; then + echo -e "${BRIGHT_CYAN}smash${RESET}: Find and optionally kill processes by name" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}smash${RESET} ${BRIGHT_MAGENTA}<${BRIGHT_YELLOW}process_name${BRIGHT_MAGENTA}>${RESET}" + echo -e "${BRIGHT_WHITE}Examples:${RESET}" + echo -e " ${BRIGHT_CYAN}smash${RESET} ${BRIGHT_YELLOW}vim${RESET}" + return 1 + fi + + local T_PROC=$1 + local T_PIDS=($(pgrep -i "$T_PROC")) + if [[ "${#T_PIDS[@]}" -ge 1 ]]; then + echo "Found the following processes:" + for pid in "${T_PIDS[@]}"; do + echo "$pid" "$(command ps -p "$pid" -o comm= | awk -F'/' '{print $NF}')" | column -t + done + if ask "Kill them?" N; then + for pid in "${T_PIDS[@]}"; do + echo "Killing ${pid}..." + # Try SIGTERM first, then escalate if process survives + builtin kill -15 "$pid" 2>/dev/null + sleep 2 + if builtin kill -0 "$pid" 2>/dev/null; then + builtin kill -2 "$pid" 2>/dev/null + sleep 2 + if builtin kill -0 "$pid" 2>/dev/null; then + builtin kill -1 "$pid" 2>/dev/null + sleep 2 + if builtin kill -0 "$pid" 2>/dev/null; then + echo "Cannot terminate ${pid}" >&2 + continue + fi + fi + fi + done + else + echo "Exiting..." + return 0 + fi + else + echo "No processes found for: $1" >&2 && return 1 + fi +} + +# Automatically downloads based on URL by dynamically choosing the appropriate command +# Detects the domain from the URL and chooses the appropriate download command +# --- +# IMPORTANT: Always ensure that you have the legal right and ethical justification +# to download media from various services. Respect copyright laws and terms of service +# agreements. Use of this script should comply with all applicable regulations. +alias d='download' +function download() { + local URL="${1}" + local DOWNLOAD_PATH="${2:-.}" + + # Check if we are in a graphical environment and Desktop exists + if [[ "${DOWNLOAD_PATH}" == "." ]] && ([[ -n "$DISPLAY" ]] || [[ -n "$WAYLAND_DISPLAY" ]]) && [[ -n "$XDG_CURRENT_DESKTOP" ]] && hascommand xdg-user-dir; then + DOWNLOAD_PATH="$(xdg-user-dir DOWNLOAD)" + fi + + # Display help if no URL provided or help requested + if [[ -z "${URL}" ]] || [[ "${URL}" == "--help" ]] || [[ "${URL}" == "-h" ]]; then + echo -e "${BRIGHT_CYAN}d${RESET}: Download from URL using the best available tool" + echo -en "${BRIGHT_WHITE}Tools:${RESET} ${BRIGHT_CYAN}axel${RESET}, ${BRIGHT_CYAN}aria2c${RESET}, ${BRIGHT_CYAN}wget${RESET}, ${BRIGHT_CYAN}curl${RESET}," + echo -e " ${BRIGHT_MAGENTA}yt-dlp${RESET}, ${BRIGHT_MAGENTA}spotdl${RESET}, ${BRIGHT_MAGENTA}tidal-dl${RESET}, ${BRIGHT_MAGENTA}scdl${RESET}" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}d${RESET} ${BRIGHT_MAGENTA}<${BRIGHT_YELLOW}URL${BRIGHT_MAGENTA}>${RESET} ${BRIGHT_MAGENTA}[${BRIGHT_GREEN}download_path${BRIGHT_MAGENTA}]${RESET}" + echo -e "${BRIGHT_WHITE}Examples:${RESET}" + echo -e " ${BRIGHT_CYAN}d${RESET} ${BRIGHT_YELLOW}https://example.com/file.zip${RESET} ${BRIGHT_BLUE}# Direct download${RESET}" + echo -e " ${BRIGHT_CYAN}d${RESET} ${BRIGHT_YELLOW}https://youtube.com/watch?v=...${RESET} ${BRIGHT_BLUE}# YouTube video${RESET}" + echo -e " ${BRIGHT_CYAN}d${RESET} ${BRIGHT_YELLOW}https://open.spotify.com/track/...${RESET} ${BRIGHT_BLUE}# Spotify track${RESET}" + echo -e " ${BRIGHT_CYAN}d${RESET} ${BRIGHT_YELLOW}https://soundcloud.com/...${RESET} ${BRIGHT_BLUE}# SoundCloud${RESET}" + echo -e " ${BRIGHT_CYAN}d${RESET} ${BRIGHT_YELLOW}https://example.com/file.zip${RESET} ${BRIGHT_GREEN}~/Downloads${RESET} ${BRIGHT_BLUE}# Custom path${RESET}" + return 1 + + # Spotify URLs + elif [[ "${URL}" =~ ^https://open.spotify.com/ ]]; then + + # spotDL finds songs from Spotify playlists on YouTube and downloads + # them along with album art, lyrics, and metadata + # Link: https://github.com/spotDL/spotify-downloader + if hascommand spotdl; then + echo -e "${BRIGHT_YELLOW}Using spotdl for Spotify URL...${RESET}" + (cd "${DOWNLOAD_PATH}" && spotdl --bitrate 320k "${URL}") || echo -e "${BRIGHT_RED}Download failed.${RESET}" + + else + echo -e "${BRIGHT_RED}ERROR: ${BRIGHT_CYAN}No suitable Spotify download tool found${RESET}" + return 1 + fi + + # Tidal URLs + elif [[ "${URL}" =~ ^https://tidal.com/ ]]; then + + # TIDAL Downloader Next Generation downloads songs and videos from TIDAL + # https://github.com/exislow/tidal-dl-ng + if hascommand tidal-dl-ng; then + echo -e "${BRIGHT_YELLOW}Using tidal-dl-ng for Tidal URL...${RESET}" + (cd "${DOWNLOAD_PATH}" && tidal-dl-ng dl "${URL}") || echo -e "${BRIGHT_RED}Download failed.${RESET}" + + # Tidal-Media-Downloader» lets you download videos and tracks from Tidal + # https://github.com/yaronzz/Tidal-Media-Downloader + elif hascommand tidal-dl; then + echo -e "${BRIGHT_YELLOW}Using tidal-dl for Tidal URL (ensure logged in)...${RESET}" + (cd "${DOWNLOAD_PATH}" && tidal-dl "${URL}") || echo -e "${BRIGHT_RED}Download failed.${RESET}" + + else + echo -e "${BRIGHT_RED}ERROR: ${BRIGHT_CYAN}No suitable Tidal download tool found${RESET}" + return 1 + fi + + # Soundcloud + scdl is a script is able to download music from SoundCloud + # and can also set the id3tag to the downloaded music file + # Link: https://github.com/scdl-org/scdl + elif [[ "${URL}" =~ ^https?://(www\.)?soundcloud.com/ ]] && hascommand scdl; then + # Download using the scdl script + echo -e "${BRIGHT_YELLOW}Using scdl for SoundCloud URL...${RESET}" + (scdl -l "${URL}" --path "${DOWNLOAD_PATH}" --onlymp3) || echo -e "${BRIGHT_RED}Download failed.${RESET}" + + # SoundCloud, Youtube Music, Bandcamp and other audio-focused platforms + elif [[ "${URL}" =~ ^https?://(www\.)?(soundcloud\.com|bandcamp\.com|mixcloud\.com|audiomack\.com|audius\.co|hearthis\.at|play\.fm|clyp\.it|indieshuffle\.com|music\.youtube\.com)/ ]]; then + # yt-dlp is a feature-rich command-line audio/video downloader + # with support for thousands of sites + # Link: https://github.com/yt-dlp/yt-dlp + if hascommand yt-dlp; then + echo -e "${BRIGHT_YELLOW}Using yt-dlp for the audio URL...${RESET}" + + if ask "Do you want to download the audio in MP3 format (choosing No will download the highest quality original format)?" Y; then + (cd "${DOWNLOAD_PATH}" && yt-dlp -f 'bestaudio/best' --extract-audio --audio-format mp3 --audio-quality 0 "${URL}") || echo -e "${BRIGHT_RED}Download failed.${RESET}" + else + (cd "${DOWNLOAD_PATH}" && yt-dlp -f 'bestaudio/best' --extract-audio --audio-quality 0 "${URL}") || echo -e "${BRIGHT_RED}Download failed.${RESET}" + fi + + # youtube-dl downloads videos from youtube.com or other video platforms + # Link: https://github.com/ytdl-org/youtube-dl + elif hascommand youtube-dl; then + echo -e "${BRIGHT_YELLOW}Using youtube-dl for the audio URL...${RESET}" + + if ask "Do you want to download the audio in MP3 format (choosing No will download the highest quality original format)?" Y; then + (cd "${DOWNLOAD_PATH}" && youtube-dl -f 'bestaudio' --extract-audio --audio-format mp3 --audio-quality 0 "${URL}") || echo -e "${BRIGHT_RED}Download failed.${RESET}" + else + (cd "${DOWNLOAD_PATH}" && youtube-dl -f 'bestaudio' --extract-audio --audio-format best --audio-quality 0 "${URL}") || echo -e "${BRIGHT_RED}Download failed.${RESET}" + fi + + else + echo -e "${BRIGHT_RED}ERROR: ${BRIGHT_CYAN}No suitable audio download tool found${RESET}" + return 1 + fi + + # Simple video platforms (Odysee, Rumble, Bitchute, etc) - basic format selection + elif [[ "${URL}" =~ ^https?://(www\.)?(bitchute\.com|lbry\.tv|metacafe\.com|odysee\.com|peertube\.[a-zA-Z]+|rumble\.com|streamable\.com|ted\.com|viddler\.com|wistia\.com)/ ]]; then + + # Found yt-dlp command-line audio/video downloader + if hascommand yt-dlp; then + echo -e "${BRIGHT_YELLOW}Using yt-dlp for the video URL...${RESET}" + (cd "${DOWNLOAD_PATH}" && yt-dlp --format "best" --progress --no-warnings --geo-bypass "${URL}") || echo -e "${BRIGHT_RED}Download failed.${RESET}" + + # Found youtube-dl command-line audio/video downloader + elif hascommand youtube-dl; then + echo -e "${BRIGHT_YELLOW}Using youtube-dl for the video URL...${RESET}" + (cd "${DOWNLOAD_PATH}" && youtube-dl --format "best" --progress --no-warnings --geo-bypass "${URL}") || echo -e "${BRIGHT_RED}Download failed.${RESET}" + + else + echo -e "${BRIGHT_RED}ERROR: ${BRIGHT_CYAN}No suitable video download tool found${RESET}" + return 1 + fi + + # Major streaming platforms (YouTube, Twitch, etc) - advanced format handling + elif [[ "${URL}" =~ ^https?://(www\.)?(youtube\.com|youtu\.be|dailymotion\.com|facebook\.com|instagram\.com|linkedin\.com|ok\.ru|reddit\.com|tiktok\.com|tumblr\.com|twitch\.tv|twitter\.com|vevo\.com|vimeo\.com|vk\.com)/ ]]; then + + # Found yt-dlp command-line audio/video downloader + if hascommand yt-dlp; then + echo -e "${BRIGHT_YELLOW}Using yt-dlp for the video URL...${RESET}" + + if ask "Do you want to download the highest available video quality (choosing No will limit to 1080p)?" N; then + (cd "${DOWNLOAD_PATH}" && yt-dlp -o "%(title)s.%(ext)s" -f 'bestvideo[ext=mp4]+bestaudio' --merge-output-format mp4 "${URL}" || yt-dlp -o "video.%(ext)s" -f 'bestvideo[ext=mp4]+bestaudio' --merge-output-format mp4 "${URL}") || echo -e "${BRIGHT_RED}Download failed.${RESET}" + else + (cd "${DOWNLOAD_PATH}" && yt-dlp -o "%(title)s.%(ext)s" -f 'bestvideo[ext=mp4][height<=1080]+bestaudio' --merge-output-format mp4 "${URL}" || yt-dlp -o "video.%(ext)s" -f 'bestvideo[ext=mp4][height<=1080]+bestaudio' --merge-output-format mp4 "${URL}") || echo -e "${BRIGHT_RED}Download failed.${RESET}" + fi + + # Found youtube-dl command-line audio/video downloader + elif hascommand youtube-dl; then + echo -e "${BRIGHT_YELLOW}Using youtube-dl for the video URL...${RESET}" + + if ask "Do you want to download the highest available video quality (choosing No will limit to 1080p)?" N; then + (cd "${DOWNLOAD_PATH}" && youtube-dl --format 'best[vcodec*=avc]+bestaudio' "${URL}") || echo -e "${BRIGHT_RED}Download failed.${RESET}" + else + (cd "${DOWNLOAD_PATH}" && youtube-dl --format 'best[vcodec*=avc][height<=1080]+bestaudio' "${URL}") || echo -e "${BRIGHT_RED}Download failed.${RESET}" + fi + + else + echo -e "${BRIGHT_RED}ERROR: ${BRIGHT_CYAN}No suitable video download tool found${RESET}" + return 1 + fi + + # Download using the detected tool + else + if hascommand axel; then + # Axel accelerates the download process by using multiple connections + # per file, and can also balance the load between different servers + # Link: https://github.com/axel-download-accelerator/axel + echo -e "${BRIGHT_YELLOW}Downloading using axel...${RESET}" + (cd "${DOWNLOAD_PATH}" && command axel -a -n 10 "${URL}") || echo -e "${BRIGHT_RED}Download failed.${RESET}" + + elif hascommand aria2c; then + # aria2c is a lightweight multi-protocol & multi-source command-line + # download utility that supports HTTP/HTTPS, FTP, SFTP, BitTorrent, and + # Metalink with multiple connections and enhanced control over connections + # Link: https://aria2.github.io/ + echo -e "${BRIGHT_YELLOW}Downloading using aria2c...${RESET}" + command aria2c --max-connection-per-server=5 --continue=true --async-dns=false --dir="${DOWNLOAD_PATH}" "${URL}" || echo -e "${BRIGHT_RED}Download failed.${RESET}" + + elif hascommand curl; then + # curl supports data transfer from or to a server using multiple protocols + # like HTTP, HTTPS, and FTP, and features resuming and redirect following + # Link: https://curl.se/ + echo -e "${BRIGHT_YELLOW}Downloading using curl...${RESET}" + (cd "${DOWNLOAD_PATH}" && command curl -C - -L -O "${URL}") || echo -e "${BRIGHT_RED}Download failed.${RESET}" + + elif hascommand wget; then + # wget is a non-interactive command-line file downloader for HTTP, HTTPS, + # and FTP that supports resuming downloads on more unstable connections + # Link: https://www.gnu.org/software/wget/ + echo -e "${BRIGHT_YELLOW}Downloading using wget...${RESET}" + command wget --continue -P "${DOWNLOAD_PATH}" "${URL}" || echo -e "${BRIGHT_RED}Download failed.${RESET}" + + else + echo -e "${BRIGHT_RED}ERROR: No suitable download tool found${RESET}" + return 1 + fi + fi +} + +# Extracts any archive(s) +function extract() { + # If no archive is specified or --help or -h is passed, show help text + if [[ -z "${1}" ]] || [[ "${1}" == "--help" ]] || [[ "${1}" == "-h" ]]; then + echo -e "${BRIGHT_CYAN}extract${RESET}: Extract one or multiple archive files" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}extract${RESET} ${BRIGHT_MAGENTA}<${BRIGHT_YELLOW}archive${BRIGHT_MAGENTA}>${RESET}..." + echo -e "${BRIGHT_WHITE}Supports:${RESET} .tar.gz .tar.bz2 .tar .tgz .tbz2 .gz .bz2 .zip .rar .7z .Z" + echo -e "${BRIGHT_WHITE}Examples:${RESET}" + echo -e " ${BRIGHT_CYAN}extract${RESET} ${BRIGHT_YELLOW}backup.tar.gz${RESET} ${BRIGHT_BLUE}# Extract tarball${RESET}" + echo -e " ${BRIGHT_CYAN}extract${RESET} ${BRIGHT_YELLOW}archive.zip${RESET} ${BRIGHT_BLUE}# Extract zip${RESET}" + echo -e " ${BRIGHT_CYAN}extract${RESET} ${BRIGHT_YELLOW}data.7z${RESET} ${BRIGHT_BLUE}# Extract 7-zip${RESET}" + echo -e " ${BRIGHT_CYAN}extract${RESET} ${BRIGHT_YELLOW}file1.tar.gz file2.zip${RESET} ${BRIGHT_BLUE}# Multiple archives${RESET}" + return 1 + fi + + # Loop through each archive provided as an argument + for archive in "$@"; do + # Check if the file exists + if [ -f "${archive}" ] ; then + # Switch case to handle various archive types + case "${archive}" in + *.tar.bz2) tar xvjf "${archive}" ;; + *.tar.gz) tar xvzf "${archive}" ;; + *.bz2) bunzip2 "${archive}" ;; + *.rar) rar x "${archive}" ;; + *.gz) gunzip "${archive}" ;; + *.tar) tar xvf "${archive}" ;; + *.tbz2) tar xvjf "${archive}" ;; + *.tgz) tar xvzf "${archive}" ;; + *.zip) unzip "${archive}" ;; + *.Z) uncompress "${archive}" ;; + *.7z) 7z x "${archive}" ;; + *) echo -e "${BRIGHT_RED}Error: ${BRIGHT_CYAN}Unknown archive type ${BRIGHT_YELLOW}${archive##*.}${RESET}" ;; + esac + else + echo -e "${BRIGHT_RED}Error: ${BRIGHT_CYAN}The file ${BRIGHT_YELLOW}${archive}${BRIGHT_CYAN} is not valid or does not exist${RESET}" + fi + done +} + +# Generate a random password +# Pass the number of characters for the password on the command line +# Add the parameter +s to include symbols and -s to exclude them +# Example: genpw +# Example: genpw +s 24 +# Example: genpw 8 +# Example: genpw 12 -s +# HINT: Check passwords with cracklib: echo "1234abc" | cracklib-check +# Link: https://www.cyberciti.biz/security/linux-password-strength-checker/ +function genpw() { + # Help text + if [[ "${1}" == "--help" ]] || [[ "${1}" == "-h" ]]; then + echo -e "${BRIGHT_CYAN}genpw${RESET}: Generate secure random passwords" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}genpw${RESET} ${BRIGHT_MAGENTA}[${BRIGHT_GREEN}length${BRIGHT_MAGENTA}]${RESET} ${BRIGHT_MAGENTA}[${BRIGHT_GREEN}-s|+s${BRIGHT_MAGENTA}]${RESET}" + echo -e "${BRIGHT_WHITE}Options:${RESET}" + echo -e " ${BRIGHT_GREEN}+s${RESET} Include symbols" + echo -e " ${BRIGHT_GREEN}-s${RESET} Exclude symbols" + echo -e "${BRIGHT_WHITE}Examples:${RESET}" + echo -e " ${BRIGHT_CYAN}genpw${RESET} ${BRIGHT_BLUE}# Interactive (default 16 chars)${RESET}" + echo -e " ${BRIGHT_CYAN}genpw${RESET} ${BRIGHT_GREEN}24${RESET} ${BRIGHT_BLUE}# 24 character passwords${RESET}" + echo -e " ${BRIGHT_CYAN}genpw${RESET} ${BRIGHT_GREEN}32 +s${RESET} ${BRIGHT_BLUE}# 32 chars with symbols${RESET}" + echo -e " ${BRIGHT_CYAN}genpw${RESET} ${BRIGHT_GREEN}16 -s${RESET} ${BRIGHT_BLUE}# 16 chars without symbols${RESET}" + return 0 + fi + + local _PASSWORD_LENGTH + local _PASSWORD_SYMBOLS + + if [[ ${1} -gt 0 ]]; then + _PASSWORD_LENGTH=${1} + elif [[ ${2} -gt 0 ]]; then + _PASSWORD_LENGTH=${2} + else + read -e -i "16" -p "How many characters? " _PASSWORD_LENGTH + if [[ ! ${_PASSWORD_LENGTH} -gt 0 ]]; then + _PASSWORD_LENGTH=16 + fi + fi + + if [[ ${1} == '-s' ]] || [[ ${2} == '-s' ]]; then + _PASSWORD_SYMBOLS=false + elif [[ ${1} == '+s' ]] || [[ ${2} == '+s' ]]; then + _PASSWORD_SYMBOLS=true + else + if ask "Do you want to include symbols?" Y; then + _PASSWORD_SYMBOLS=true + fi + fi + + if hascommand --strict apg; then + if [[ ${_PASSWORD_SYMBOLS} == true ]]; then + apg -a 1 -n 10 -m ${_PASSWORD_LENGTH} -l + else + apg -a 0 -n 10 -m ${_PASSWORD_LENGTH} -l + fi + elif hascommand --strict pwgen; then + if [[ ${_PASSWORD_SYMBOLS} == true ]]; then + pwgen --capitalize --numerals --symbols --secure -C ${_PASSWORD_LENGTH} 40 + else + pwgen --capitalize --numerals --secure -C ${_PASSWORD_LENGTH} 40 + fi + elif hascommand --strict gpg; then + for ((n=0;n<10;n++)); do + if [[ ${_PASSWORD_SYMBOLS} == true ]]; then + gpg --gen-random 1 1024 | tr -dc a-zA-Z0-9'`~!@#$%^&*-_=+()[]{}|;:",.?<>/\\'"'" | head -c${_PASSWORD_LENGTH}; echo + else + gpg --gen-random 1 1024 | tr -dc a-zA-Z0-9 | head -c${_PASSWORD_LENGTH}; echo + fi + done + elif hascommand --strict openssl; then + for ((n=0;n<10;n++)); do + if [[ ${_PASSWORD_SYMBOLS} == true ]]; then + openssl rand 1024 | tr -dc a-zA-Z0-9'`~!@#$%^&*-_=+()[]{}|;:",.?<>/\\'"'" | head -c${_PASSWORD_LENGTH}; echo + else + openssl rand 1024 | tr -dc a-zA-Z0-9 | head -c${_PASSWORD_LENGTH}; echo + fi + done + else + for ((n=0;n<10;n++)); do + if [[ ${_PASSWORD_SYMBOLS} == true ]]; then + cat /dev/urandom | tr -dc a-zA-Z0-9'`~!@#$%^&*-_=+()[]{}|;:",.?<>/\\'"'" | head -c${_PASSWORD_LENGTH}; echo + else + cat /dev/urandom | tr -dc a-zA-Z0-9 | head -c${_PASSWORD_LENGTH}; echo + fi + done + fi +} + +# Schedule the computer to auto reboot (defaults to 4:00 AM) +function rebootlater() { + # Show help text + if [[ "${1}" == "--help" ]] || [[ "${1}" == "-h" ]]; then + echo -e "${BRIGHT_CYAN}rebootlater${RESET}: Schedule the computer to auto reboot at a specified time" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}rebootlater${RESET} ${BRIGHT_MAGENTA}[${BRIGHT_GREEN}time${BRIGHT_MAGENTA}]${RESET}" + echo -e "${BRIGHT_WHITE}Examples:${RESET}" + echo -e " ${BRIGHT_CYAN}rebootlater${RESET} ${BRIGHT_BLUE}# Defaults to 4:00 AM${RESET}" + echo -e " ${BRIGHT_CYAN}rebootlater${RESET} ${BRIGHT_YELLOW}23:30${RESET} ${BRIGHT_BLUE}# Reboot at 11:30 PM${RESET}" + echo -e " ${BRIGHT_CYAN}sudo shutdown -c${RESET} ${BRIGHT_BLUE}# Cancel a scheduled reboot${RESET}" + return 0 + fi + + sudo shutdown -r "${1:-04:00}" +} + +# See what command you are using the most (this parses the history command) +# Usage: mostused [num_items] +function mostused() { + local NUM_ITEMS="${1:-10}" # Default to 10 if NUM_ITEMS is not specified + history \ + | awk ' { a[$4]++ } END { for ( i in a ) print a[i], i | "sort -rn | head -n'"${NUM_ITEMS}"'"}' \ + | awk '$1 > max{ max=$1} { bar=""; i=s=10*$1/max;while(i-->0)bar=bar"#"; printf "%25s %15d %s %s", $2, $1,bar, "\n"; }' +} + +# Start a program but immediately disown it and detach it from the terminal +function runfree() { + "$@" > /dev/null 2>&1 & disown +} + +# Sends a desktop notification when a command finishes its execution +# Examples: sleep 1; alert # Notifies when the sleep command completes +# make && alert 'Success' || alert 'Error' # Notify result status +# tar -czvf file.tar.gz && alert # Notifies if the command succeeds +# false || alert "There was an error" # Notifies if the command fails +# Link: https://askubuntu.com/questions/423646/use-of-default-alias-alert +function alert() { + # Determine the icon based on the exit status of the last command + # If exit status is zero (success), use 'terminal' as icon; otherwise use 'error' + local ICON="$([[ $? == 0 ]] && echo terminal || echo error)" + + # Extract the last executed command from the history + # Remove leading numbers and trailing 'alert' command if exists + local LAST_COMMAND="$(history | tail -n1 | sed -e 's/^\s*[0-9]\+\s*//;s/[;&|]\s*alert$//')" + + # Send the desktop notification with highest urgency level ("critical") + notify-send --urgency=critical -i "${ICON}" "${LAST_COMMAND}" +} + +# Format a string that is safe to be used in regular expressions +function regexformat() { + # Escape special regex characters for use in patterns + # Note: In POSIX bracket expressions, backslash is literal, not an escape char. + # Place backslash last to make intent explicit; parens/space don't need escaping. + echo -n "$(printf '%s' "${1}" | sed 's/[.[() *^$+?{|\\]/\\&/g')" +} + +# Trim leading and trailing characters on the clipboard +function trimcb() { + # Fetch the current clipboard content and declare it as a local variable + local CONTENT_TO_TRIM=$(clipboard) + + # If clipboard content is not empty, trim it and send it back to the clipboard + if [[ -n "${CONTENT_TO_TRIM}" ]]; then + echo -e "${CONTENT_TO_TRIM}" | sed 's/^[ \t]*//;s/[ \t]*$//' | clipboard + fi +} + +# Long format directory listing with color columns (only requires gawk) +function llcolor { + if hascommand --strict gawk; then + # Show long directory listings with color columns + command ls -l --all --classify --group-directories-first --human-readable --color=always "$@" | awk ' + BEGIN { + FPAT = "([[:space:]]*[^[:space:]]+)"; + OFS = ""; + } + { + $1 = "\033[0;37m" $1 "\033[0m"; + $2 = "\033[0;34m" $2 "\033[0m"; + $3 = "\033[0;35m" $3 "\033[0m"; + $4 = "\033[0;31m" $4 "\033[0m"; + $5 = "\033[0;33m" $5 "\033[0m"; + $6 = "\033[0;32m" $6 "\033[0m"; + $7 = "\033[0;32m" $7 "\033[0m"; + $8 = "\033[0;36m" $8 "\033[0m"; + print + } + ' + else # Gawk not installed... + # Show long directory listings with highest compatibility + command ls -Fls "$@" + fi +} + +# Commands pushd and popd now output the directory stack after modification +# and also prevents duplicate directories being added to the directory stack +function pushd() { + builtin pushd "${@}" > /dev/null + dirsdedup + echo "Directory Stack:" + dirs -v +} + +function popd() { + builtin popd "${@}" > /dev/null + echo "Directory Stack:" + dirs -v +} + +# Remove duplicate directories in the directory stack for pushd +function dirsdedup() { + # Declare an array to hold the new directory stack without duplicates + declare -a NEW_STACK=() + # Declare a copy of the current directory stack, excluding the first element + declare -a CURRENT_STACK_COPY=("${DIRSTACK[@]:1}") + # Declare an associative array to keep track of directories seen + declare -A SEEN_DIRECTORIES + # Local variables for loop iteration + local DIRECTORY INDEX + # Mark the current working directory as seen + SEEN_DIRECTORIES[$PWD]=1 + + # Iterate through the copied directory stack + for DIRECTORY in "${CURRENT_STACK_COPY[@]}" + do + # If the directory has not been seen before, add it to the new stack + if [ -z "${SEEN_DIRECTORIES[$DIRECTORY]}" ]; then + NEW_STACK+=("$DIRECTORY") + SEEN_DIRECTORIES[$DIRECTORY]=1 + fi + done + + # Rebuild the directory stack from the new stack, in reverse order + builtin dirs -c + for ((INDEX=${#NEW_STACK[@]}-1; INDEX>=0; INDEX--)) + do + builtin pushd -n "${NEW_STACK[INDEX]}" >/dev/null + done +} + +# View a comma delimited (.CSV) file +function csvview() { + # Check for the presence of arguments + if [[ $# -eq 0 ]] || [[ "${1}" == "--help" ]] || [[ "${1}" == "-h" ]]; then + echo -e "${BRIGHT_CYAN}csvview${RESET}: View comma-delimited (.CSV) files" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}csvview${RESET} ${BRIGHT_MAGENTA}<${BRIGHT_YELLOW}file.csv${BRIGHT_MAGENTA}>${RESET}..." + echo -e "${BRIGHT_WHITE}Examples:${RESET}" + echo -e " ${BRIGHT_CYAN}csvview${RESET} ${BRIGHT_YELLOW}data.csv${RESET}" + echo -e " ${BRIGHT_CYAN}csvview${RESET} ${BRIGHT_YELLOW}file1.csv file2.csv${RESET} ${BRIGHT_BLUE}# Multiple files${RESET}" + return 1 + fi + + # Loop through all the arguments + for FILE in "${@}"; do + # Check if file exists + if [[ ! -f "${FILE}" ]]; then + echo -e "${BRIGHT_RED}Error: ${BRIGHT_CYAN}The file ${BRIGHT_YELLOW}${FILE}${BRIGHT_CYAN} does not exist${RESET}" + continue + # Check if file is readable + elif [[ ! -r "${FILE}" ]]; then + echo -e "${BRIGHT_RED}Error: ${BRIGHT_CYAN}The file ${BRIGHT_YELLOW}${FILE}${BRIGHT_CYAN} is not readable${RESET}" + continue + fi + + # Display the formatted CSV file + command cat "${FILE}" | command sed 's/,/ ,/g' | column -t -s, | command less -S + done +} + +# Send file(s) to the trash +# Link: https://www.tecmint.com/trash-cli-manage-linux-trash-from-command-line/ +function trash() { + # Check for the presence of arguments or help requested + if [[ $# -eq 0 ]] || [[ "${1}" == "--help" ]] || [[ "${1}" == "-h" ]]; then + echo -e "${BRIGHT_CYAN}trash${RESET}: Send files to the trash" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}trash${RESET} ${BRIGHT_MAGENTA}<${BRIGHT_YELLOW}file${BRIGHT_MAGENTA}>${RESET}..." + echo -e "${BRIGHT_WHITE}Examples:${RESET}" + echo -e " ${BRIGHT_CYAN}trash${RESET} ${BRIGHT_YELLOW}oldfile.txt${RESET}" + echo -e " ${BRIGHT_CYAN}trash${RESET} ${BRIGHT_YELLOW}file1.txt file2.txt${RESET} ${BRIGHT_BLUE}# Multiple files${RESET}" + return 1 + fi + + # Check if trash-cli exists... + # https://github.com/andreafrancia/trash-cli + if hascommand trash-put; then + trash-put "${@}" && return 0 + + # Check if rem exists... + # Link: https://github.com/quackduck/rem + elif hascommand rem; then + rem "${@}" && return 0 + + # Check if gio trash exists (glib2)... + # Link: https://wiki.archlinux.org/title/Trash-cli#gio_trash + elif hascommand gio; then + gio trash "${@}" && return 0 + + # Check if kioclient5 exists (kde-cli-tools)... + # Link: https://wiki.archlinux.org/title/Trash-cli#kioclient5 + elif hascommand kioclient5; then + kioclient5 move "${@}" trash:/ && return 0 + + # Check for various trash directories + elif [[ -d "${XDG_DATA_HOME:-${HOME}/.local/share}/Trash/files" ]]; then + command mv -i "${@}" "${XDG_DATA_HOME:-${HOME}/.local/share}/Trash/files/" && return 0 + elif [[ -d "${XDG_DATA_HOME:-${HOME}/.local/share}/trash/files" ]]; then + command mv -i "${@}" "${XDG_DATA_HOME:-${HOME}/.local/share}/trash/files/" && return 0 + elif [[ -d "${HOME}/.Trash" ]]; then + command mv -i "${@}" "${HOME}/.Trash/" && return 0 + elif [[ -d "${HOME}/.trash" ]]; then + command mv -i "${@}" "${HOME}/.trash/" && return 0 + + # Create the trash directory per the XDG specification if none exists + else + command mkdir -p "${XDG_DATA_HOME:-${HOME}/.local/share}/Trash/files" + command mv -i "${@}" "${XDG_DATA_HOME:-${HOME}/.local/share}/Trash/files/" && return 0 + fi + + # If none of the methods succeeded, return an error + echo -e "${BRIGHT_RED}Error: ${BRIGHT_CYAN}Failed to send files to the trash${RESET}" + return 1 +} + +# Display the contents of the trash +function trashlist() { + # Help text + if [[ "${1}" == "--help" ]] || [[ "${1}" == "-h" ]]; then + echo -e "${BRIGHT_CYAN}trashlist${RESET}: List files in the trash" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}trashlist${RESET}" + echo -e "${BRIGHT_WHITE}Description:${RESET}" + echo -e " ${BRIGHT_BLUE}Auto-detects trash-cli, rem, gio, or kioclient5${RESET}" + return 0 + fi + + # Check if trash-cli exists... + # https://github.com/andreafrancia/trash-cli + if hascommand --strict trash-list; then + trash-list + # Check if rem exists... + # Link: https://github.com/quackduck/rem + elif hascommand --strict rem; then + rem -l + # Check if gio trash exists (glib2)... + # Link: https://wiki.archlinux.org/title/Trash-cli#gio_trash + elif hascommand --strict gio; then + gio list trash:/// + # Check if kioclient5 exists (kde-cli-tools)... + # Link: https://wiki.archlinux.org/title/Trash-cli#kioclient5 + elif hascommand --strict kioclient5; then + kioclient5 ls trash:/ + # Check for alternative trash directories and list files + elif [[ -d "${XDG_DATA_HOME:-${HOME}/.local/share}/Trash/files" ]]; then + ls -l "${XDG_DATA_HOME:-${HOME}/.local/share}/Trash/files/" + elif [[ -d "${XDG_DATA_HOME:-${HOME}/.local/share}/trash/files" ]]; then + ls -l "${XDG_DATA_HOME:-${HOME}/.local/share}/trash/files/" + elif [[ -d "${HOME}/.Trash" ]]; then + ls -l "${HOME}/.Trash/" + elif [[ -d "${HOME}/.trash" ]]; then + ls -l "${HOME}/.trash/" + else + echo -e "${BRIGHT_RED}Error: ${BRIGHT_CYAN}No trash directory found${RESET}" + fi +} + +# Empty and permanently delete all the files in the trash +function trashempty() { + # Help text + if [[ "${1}" == "--help" ]] || [[ "${1}" == "-h" ]]; then + echo -e "${BRIGHT_CYAN}trashempty${RESET}: Permanently delete all files in the trash" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}trashempty${RESET}" + echo -e "${BRIGHT_WHITE}Description:${RESET}" + echo -e " ${BRIGHT_BLUE}Prompts for confirmation before deleting${RESET}" + echo -e " ${BRIGHT_BLUE}Auto-detects trash-cli, rem, gio, or kioclient5${RESET}" + return 0 + fi + + # Ask for user confirmation before deleting trash + if ask "${BRIGHT_WHITE}Are you sure you want to ${BRIGHT_MAGENTA}permanently delete${BRIGHT_WHITE} all the files in the trash? ${BRIGHT_RED}This action cannot be undone.${RESET}" "N"; then + # Check if trash-cli exists... + # https://github.com/andreafrancia/trash-cli + if hascommand --strict trash-empty; then + trash-empty + # Check if rem exists... + # Link: https://github.com/quackduck/rem + elif hascommand --strict rem; then + rem --empty + # Check if gio trash exists (glib2)... + # Link: https://wiki.archlinux.org/title/Trash-cli#gio_trash + elif hascommand --strict gio; then + gio trash --empty + # Check if kioclient5 exists (kde-cli-tools)... + # Link: https://wiki.archlinux.org/title/Trash-cli#kioclient5 + elif hascommand --strict kioclient5; then + kioclient5 empty trash:/ + # Check for alternative trash directories and delete files + elif [[ -d "${XDG_DATA_HOME:-${HOME}/.local/share}/Trash/files" ]]; then + local _TRASH="${XDG_DATA_HOME:-${HOME}/.local/share}/Trash/files" + rm -rf "${_TRASH}"/{..?*,.[!.]*,*} 2>/dev/null + elif [[ -d "${XDG_DATA_HOME:-${HOME}/.local/share}/trash/files" ]]; then + local _TRASH="${XDG_DATA_HOME:-${HOME}/.local/share}/trash/files" + rm -rf "${_TRASH}"/{..?*,.[!.]*,*} 2>/dev/null + elif [[ -d "${HOME}/.Trash" ]]; then + rm -rf "${HOME}/.Trash"/{..?*,.[!.]*,*} 2>/dev/null + elif [[ -d "${HOME}/.trash" ]]; then + rm -rf "${HOME}/.trash"/{..?*,.[!.]*,*} 2>/dev/null + else + # No supported method found for emptying trash + echo -e "${BRIGHT_RED}Error: ${BRIGHT_CYAN}No trash directory or supported application found${RESET}" + fi + else + # Operation was cancelled by the user + echo -e "${BRIGHT_RED}Operation cancelled.${RESET}" + fi +} + +# Restore the trash only is trash-cli is installed +# trash-cli - Command Line Interface to FreeDesktop.org Trash +# Link: https://github.com/andreafrancia/trash-cli +if hascommand --strict restore-trash; then + alias trashrestore='restore-trash' +elif hascommand --strict trash-restore; then + alias trashrestore='trash-restore' +fi + +# Recursively remove all empty directories from a given path +# Syntax: rmempty [directory] +function rmempty() { + # Show help if --help or -h is passed + if [[ "${1}" == "--help" ]] || [[ "${1}" == "-h" ]]; then + echo -e "${BRIGHT_CYAN}rmempty${RESET}: Recursively remove all empty directories" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}rmempty${RESET} ${BRIGHT_MAGENTA}[${BRIGHT_GREEN}directory${BRIGHT_MAGENTA}]${RESET}" + echo + echo -e "${BRIGHT_WHITE}Options:${RESET}" + echo -e " ${BRIGHT_YELLOW}-h${RESET}, ${BRIGHT_YELLOW}--help${RESET} Show this help message" + echo + echo -e "${BRIGHT_WHITE}Description:${RESET}" + echo -e " Finds and removes all empty directories within the specified path." + echo -e " If no directory is specified, uses the current working directory." + echo -e " You will be prompted for confirmation before any directories are removed." + echo + echo -e "${BRIGHT_WHITE}Examples:${RESET}" + echo -e " ${BRIGHT_CYAN}rmempty${RESET} ${BRIGHT_BLUE}# Current directory${RESET}" + echo -e " ${BRIGHT_CYAN}rmempty${RESET} ${BRIGHT_YELLOW}/path/to/dir${RESET} ${BRIGHT_BLUE}# Specified path${RESET}" + return 0 + fi + + # Use the provided directory or default to current directory + local TARGET_DIR="${1:-.}" + + # Resolve to absolute path + local ABSOLUTE_PATH + ABSOLUTE_PATH=$(realpath "${TARGET_DIR}" 2>/dev/null) + + # Check if realpath succeeded + if [[ -z "${ABSOLUTE_PATH}" ]]; then + echo -e "${BRIGHT_RED}Error:${RESET} ${BRIGHT_CYAN}Could not resolve path:${RESET} ${BRIGHT_YELLOW}${TARGET_DIR}${RESET}" + return 1 + fi + + # Check if the target directory exists + if [[ ! -d "${ABSOLUTE_PATH}" ]]; then + echo -e "${BRIGHT_RED}Error:${RESET} ${BRIGHT_CYAN}Directory does not exist:${RESET} ${BRIGHT_YELLOW}${ABSOLUTE_PATH}${RESET}" + return 1 + fi + + # Check if the target directory is readable + if [[ ! -r "${ABSOLUTE_PATH}" ]]; then + echo -e "${BRIGHT_RED}Error:${RESET} ${BRIGHT_CYAN}Directory is not readable:${RESET} ${BRIGHT_YELLOW}${ABSOLUTE_PATH}${RESET}" + return 1 + fi + + # Find all empty directories (depth-first order so nested empties are found) + local EMPTY_DIRS + EMPTY_DIRS=$(find "${ABSOLUTE_PATH}" -type d -empty 2>/dev/null) + + # Count the empty directories + local COUNT=0 + if [[ -n "${EMPTY_DIRS}" ]]; then + COUNT=$(echo "${EMPTY_DIRS}" | wc -l) + fi + + # If no empty directories found, inform the user and exit + if [[ ${COUNT} -eq 0 ]]; then + echo -e "${BRIGHT_GREEN}No empty directories found in:${RESET} ${BRIGHT_MAGENTA}${ABSOLUTE_PATH}${RESET}" + return 0 + fi + + # Display the empty directories that will be removed + echo -e "${BRIGHT_WHITE}Found ${BRIGHT_YELLOW}${COUNT}${BRIGHT_WHITE} empty director$([[ ${COUNT} -eq 1 ]] && echo "y" || echo "ies") in:${RESET} ${BRIGHT_MAGENTA}${ABSOLUTE_PATH}${RESET}" + echo -e "${BRIGHT_BLACK}────────────────────────────────────────${RESET}" + + # Show the list of directories + while IFS= read -r DIR; do + echo -e " ${BRIGHT_CYAN}${DIR}${RESET}" + done <<< "${EMPTY_DIRS}" + + echo -e "${BRIGHT_BLACK}────────────────────────────────────────${RESET}" + + # Ask for confirmation before deleting + if ask "${BRIGHT_YELLOW}Remove all ${COUNT} empty director$([[ ${COUNT} -eq 1 ]] && echo "y" || echo "ies")?${RESET}" N; then + # Remove empty directories (depth-first ensures nested empties are removed) + local REMOVED=0 + local FAILED=0 + + while IFS= read -r DIR; do + if rmdir "${DIR}" 2>/dev/null; then + echo -e "${BRIGHT_GREEN}Removed:${RESET} ${BRIGHT_CYAN}${DIR}${RESET}" + ((REMOVED++)) + else + echo -e "${BRIGHT_RED}Failed:${RESET} ${BRIGHT_CYAN}${DIR}${RESET}" + ((FAILED++)) + fi + done <<< "$(echo "${EMPTY_DIRS}" | sort -r)" + + # Summary + echo -e "${BRIGHT_BLACK}────────────────────────────────────────${RESET}" + if [[ ${FAILED} -eq 0 ]]; then + echo -e "${BRIGHT_GREEN}Successfully removed ${REMOVED} empty director$([[ ${REMOVED} -eq 1 ]] && echo "y" || echo "ies").${RESET}" + else + echo -e "${BRIGHT_YELLOW}Removed ${REMOVED}, failed ${FAILED}.${RESET}" + fi + else + echo -e "${BRIGHT_RED}Operation cancelled.${RESET}" + return 0 + fi +} + +# Check the sha256 checksum of a file using a checksum file parameter like sha256sum.txt +function checksha256() { + if [[ "$#" -lt 2 ]] || [[ "${1}" == "--help" ]] || [[ "${1}" == "-h" ]]; then + echo -e "${BRIGHT_CYAN}checksha256${RESET}: Verify a file's SHA256 checksum against a checksum file" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}checksha256${RESET} ${BRIGHT_MAGENTA}<${BRIGHT_YELLOW}file${BRIGHT_MAGENTA}>${RESET} ${BRIGHT_MAGENTA}<${BRIGHT_YELLOW}checksum_file${BRIGHT_MAGENTA}>${RESET}" + echo -e "${BRIGHT_WHITE}Examples:${RESET}" + echo -e " ${BRIGHT_CYAN}checksha256${RESET} ${BRIGHT_YELLOW}download.iso sha256sum.txt${RESET}" + return 1 + fi + if hascommand --strict sha256sum; then + if [[ $(sha256sum "${1}" | cut -d' ' -f 1) == $(grep -Pom1 '\b[a-fA-F0-9]{64}\b' "${2}") ]]; then + echo -e "${BRIGHT_GREEN}Good" + else + echo -e "${BRIGHT_RED}Bad" + fi + else + echo -e "${BRIGHT_RED}Error: ${BRIGHT_CYAN}The application sha256sum is not installed${RESET}" + fi +} + +# If we are in a graphical desktop environment... +if [[ -n "${DISPLAY}" ]] || [[ -n "${WAYLAND_DISPLAY}" ]]; then + # Search Desktop applications + function findapps() { + local SEARCH_TERM="$1" # The keyword to search for + + # Ensure a search term is provided + if [[ -z "${SEARCH_TERM}" ]]; then + echo -e "\e[1;31mPlease provide a search term.\e[0m" + return 1 + fi + + # Search in both system-wide and user-specific application directories + local DIRECTORIES=("/usr/share/applications" "${XDG_DATA_HOME:-${HOME}/.local/share}/applications") + + echo -e "\e[1;36mApplications matching\e[0m '\e[33m${SEARCH_TERM}\e[0m':" + echo -e "\e[1;90m------------------------------------\e[0m" + + # Fields to search within the .desktop file + local FIELDS="Name|GenericName|Comment|Keywords" + + # Process each directory and collect results + while read -r LINE; do + echo "${LINE}" + done < <( + for DIR in "${DIRECTORIES[@]}"; do + if [[ -d "${DIR}" ]]; then + grep -i -l -E "(${FIELDS})=.*${SEARCH_TERM}" "${DIR}"/*.desktop | \ + while read -r DESKTOP_FILE; do + local APP_NAME FIELD_MATCH + APP_NAME=$(grep -m 1 '^Name=' "${DESKTOP_FILE}" | sed 's/Name=//') + FIELD_MATCH=$(grep -i -m 1 -E "(${FIELDS})=.*${SEARCH_TERM}" "${DESKTOP_FILE}") + echo -e "\e[1;32m✓\e[0m \e[1;33m${APP_NAME}\e[0m \e[1;35m${DESKTOP_FILE}\e[0m" + # Debug only: echo -e " \e[1;90mMatch found in: ${FIELD_MATCH}\e[0m" + done + fi + done | sort + ) + } +fi + +if hascommand --strict crontab; then + # Interactively search for cron jobs matching specified time criteria + function findcronjob() { + echo -e "${BRIGHT_CYAN}Cron Job Finder${RESET}" + echo -e "${BRIGHT_WHITE}Press Enter to match any value for a field${RESET}\n" + + # Helper function to expand ranges and build regex + build_pattern() { + local INPUT="$1" + + # Empty, "any", or "*" means match anything + if [[ -z "${INPUT}" || "${INPUT}" == "any" || "${INPUT}" == "*" ]]; then + echo '\S+' + return + fi + + local RESULT=() + IFS=',' read -ra PARTS <<< "${INPUT}" + + for PART in "${PARTS[@]}"; do + # Check if it's a range (e.g., 8-10) + if [[ "${PART}" =~ ^([0-9]+)-([0-9]+)$ ]]; then + local START="${BASH_REMATCH[1]}" + local END="${BASH_REMATCH[2]}" + for ((i = START; i <= END; i++)); do + RESULT+=("${i}") + done + else + RESULT+=("${PART}") + fi + done + + # Join with | for regex alternation + local IFS='|' + echo "${RESULT[*]}" + } + + # 1. Hour selection + local HOUR_INPUT HOUR_PATTERN + echo -e "${BRIGHT_YELLOW}Hour${RESET} (0-23, ranges/comma-separated, e.g., 8-10 or 9,15,20):" + echo -ne "${BRIGHT_CYAN}>${RESET} " + read HOUR_INPUT + HOUR_PATTERN=$(build_pattern "${HOUR_INPUT}") + + # 2. Minute selection + local MINUTE_INPUT MINUTE_PATTERN + echo -e "${BRIGHT_YELLOW}Minute${RESET} (0-59, ranges/comma-separated):" + echo -ne "${BRIGHT_CYAN}>${RESET} " + read MINUTE_INPUT + MINUTE_PATTERN=$(build_pattern "${MINUTE_INPUT}") + + # 3. Day of month selection + local DAY_INPUT DAY_PATTERN + echo -e "${BRIGHT_YELLOW}Day of month${RESET} (1-31, ranges/comma-separated):" + echo -ne "${BRIGHT_CYAN}>${RESET} " + read DAY_INPUT + DAY_PATTERN=$(build_pattern "${DAY_INPUT}") + + # 4. Month selection + local MONTH_INPUT MONTH_PATTERN + echo -e "${BRIGHT_YELLOW}Month${RESET} (1-12, ranges/comma-separated):" + echo -ne "${BRIGHT_CYAN}>${RESET} " + read MONTH_INPUT + MONTH_PATTERN=$(build_pattern "${MONTH_INPUT}") + + # 5. Day of week selection + local WEEKDAY_INPUT WEEKDAY_PATTERN + echo -e "${BRIGHT_YELLOW}Day of week${RESET} (0-7, where 0/7=Sunday, ranges/comma-separated):" + echo -ne "${BRIGHT_CYAN}>${RESET} " + read WEEKDAY_INPUT + WEEKDAY_PATTERN=$(build_pattern "${WEEKDAY_INPUT}") + + # 6. User selection + local SUDO_CHOICE + echo -e "\n${BRIGHT_YELLOW}Search root crontab with sudo?${RESET} (Y/n):" + echo -ne "${BRIGHT_CYAN}>${RESET} " + read SUDO_CHOICE + + local CRONTAB_COMMAND + if [[ "${SUDO_CHOICE}" =~ ^[Nn] ]]; then + CRONTAB_COMMAND="crontab -l" + else + CRONTAB_COMMAND="sudo crontab -l" + sudo true # Cache sudo + fi + + # Build the full regex pattern + local PATTERN="^\s*(${MINUTE_PATTERN})\s+(${HOUR_PATTERN})\s+(${DAY_PATTERN})\s+(${MONTH_PATTERN})\s+(${WEEKDAY_PATTERN})\s+" + + # Execute search + echo -e "\n${BRIGHT_GREEN}Matching cron jobs:${RESET}" + ${CRONTAB_COMMAND} 2>/dev/null | grep -E "${PATTERN}" --color=always || echo -e "${BRIGHT_YELLOW}No matching jobs found${RESET}" + } +fi + +# Searches for filenames (can use wildcards) +alias f="findfile" +function findfile() { + # Initialize the sudo prefix for running commands with elevated permissions + local SUDO_PREFIX="" + + # Check for --sudo flag and remove it from arguments if present + if [[ "$1" == "--sudo" ]]; then + SUDO_PREFIX="sudo " + shift + fi + + # Check if any filename or pattern is specified; if not, show help text + if [[ -z "$1" ]] || [[ "$1" == "--help" ]] || [[ "$1" == "-h" ]]; then + echo -e "${BRIGHT_CYAN}findfile${RESET}: Search for filenames recursively" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}findfile${RESET} ${BRIGHT_MAGENTA}[${BRIGHT_GREEN}--sudo${BRIGHT_MAGENTA}]${RESET} ${BRIGHT_MAGENTA}<${BRIGHT_YELLOW}pattern${BRIGHT_MAGENTA}>${RESET}" + echo -e "${BRIGHT_WHITE}Options:${RESET}" + echo -e " ${BRIGHT_GREEN}--sudo${RESET} Run with elevated permissions" + echo -e "${BRIGHT_WHITE}Examples:${RESET}" + echo -e " ${BRIGHT_CYAN}findfile${RESET} ${BRIGHT_YELLOW}'file.txt'${RESET}" + echo -e " ${BRIGHT_CYAN}findfile${RESET} ${BRIGHT_GREEN}--sudo${RESET} ${BRIGHT_YELLOW}'config'${RESET} ${BRIGHT_BLUE}# Search system dirs${RESET}" + return 1 + fi + + # Use fdfind if installed, else use fd or find as fallback + if hascommand --strict fdfind; then + # fdfind command options + # --type 'file' : Only search for files (not directories) + # --ignore-case : Perform a case-insensitive search + # --no-ignore : Do not respect .gitignore and .ignore files + # --hidden : Include hidden files in the search results + # --follow : Follow symlinks (WARNING: can get into an endless loop) + echo "${SUDO_PREFIX}fdfind --type 'file' --ignore-case --no-ignore --hidden '$1' ." + ${SUDO_PREFIX}fdfind --type 'file' --ignore-case --no-ignore --hidden "$1" . + + elif hascommand --strict fd; then + echo "${SUDO_PREFIX}fd --type 'file' --ignore-case --no-ignore --hidden '$1' ." + ${SUDO_PREFIX}fd --type 'file' --ignore-case --no-ignore --hidden "$1" . + + else # Use find command as a last resort + # find command options + # -type f : Search for files only, not directories + # -iname : Perform a case-insensitive search + # -follow : Dereference symlinks (follow them to their targets) + echo "${SUDO_PREFIX}find . -type f -iname '$1'" + ${SUDO_PREFIX}find . -type f -iname "$1" + fi +} + +# Searches for text in source code files located in the current path +# Supported languages: Ada, Assembly, AWK, Batch, C, COBOL, Config, C++, +# C#(CS), CSS, Dart, Emacs Lisp, Erlang, Elixir, Fortran(90, 95, older), Go, +# Groovy, Header(C, C++), HTML, Haxe, Include, INI, Arduino, Java, JavaScript, +# JSON, Kotlin, Library, Lua, M4, Objective-C, MATLAB, Makefiles, Nim, Nix, +# Pascal, PHP, Perl, Python, R, Ruby, Racket, Rust, Scala, SASS/SCSS, Shell, +# Standard ML, SQL, Swift, TCL, Templates, LaTeX, TypeScript, VB, VBA, VBS, +# VHDL, Wren, XML, YAML, Zig +function findcode() { + # Local constant for maximum line length cut-off + # NOTE: This is necessary for certain files like minified javascript + local -r LINE_LENGTH_CUTOFF=1000 + + # The prefix to prepend to search commands for elevated permissions + local SUDO_PREFIX="" + + # Check for --sudo flag and remove it from arguments if present + if [[ "$1" == "--sudo" ]]; then + SUDO_PREFIX="sudo " + shift + fi + + # If no parameter is specified, show help text + if [[ -z "$1" ]] || [[ "$1" == "--help" ]] || [[ "$1" == "-h" ]]; then + echo -e "${BRIGHT_CYAN}findcode${RESET}: Search for text in source code files recursively" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}findcode${RESET} ${BRIGHT_MAGENTA}[${BRIGHT_GREEN}--sudo${BRIGHT_MAGENTA}]${RESET} ${BRIGHT_MAGENTA}<${BRIGHT_YELLOW}pattern${BRIGHT_MAGENTA}>${RESET}" + echo -e "${BRIGHT_WHITE}Options:${RESET}" + echo -e " ${BRIGHT_GREEN}--sudo${RESET} Run with elevated permissions" + echo -e "${BRIGHT_WHITE}Examples:${RESET}" + echo -e " ${BRIGHT_CYAN}findcode${RESET} ${BRIGHT_YELLOW}'function_name'${RESET}" + echo -e " ${BRIGHT_CYAN}findcode${RESET} ${BRIGHT_YELLOW}'console\\.log\\('${RESET} ${BRIGHT_BLUE}# Regex search${RESET}" + echo -e " ${BRIGHT_CYAN}findcode${RESET} ${BRIGHT_GREEN}--sudo${RESET} ${BRIGHT_YELLOW}'todo'${RESET} ${BRIGHT_BLUE}# Search system dirs${RESET}" + return 1 + fi + + # If ripgrep is installed, use that + # Link: https://github.com/BurntSushi/ripgrep + if hascommand --strict rg; then + echo -e "${BRIGHT_CYAN}Search using ${BRIGHT_YELLOW}ripgrep${BRIGHT_CYAN}:${RESET}" + echo "${SUDO_PREFIX}rg --smart-case --no-ignore --hidden --pretty '$@' -g '!*.min.*' ." + ${SUDO_PREFIX}rg --smart-case --no-ignore --hidden --pretty "${@}" -g '!*.min.*' \ + -g '*.{ada,asm,awk,bat,c,cbl,cfg,conf,config,cpp,cpy,cs,css,dart,el,erl,ex,f,f90,f95,for,go,gradle,groovy,h,hpp,hrl,htm,html,hx,hxsl,inc,ini,ino,java,js,json,jsx,kt,lib,lua,m,m4,mat,mk,nim,nix,pascal,php,pl,plx,ps1,py,r,rb,rkt,rlib,rs,sc,scala,scss,sh,sml,sql,swift,tcl,template,tex,tpl,ts,tsx,vb,vba,vbs,vhd,vhdl,wren,xml,yaml,yml,zig}' | \ + awk -v len=$LINE_LENGTH_CUTOFF '{ $0=substr($0, 1, len); print $0 }' + + # If The Silver Searcher is installed, use that + # Link: https://github.com/ggreer/the_silver_searcher + # Hint: You can use --ignore "dir/or/file" + elif hascommand --strict ag; then + echo -e "${BRIGHT_CYAN}Search using ${BRIGHT_YELLOW}The Silver Searcher${BRIGHT_CYAN}:${RESET}" + echo "${SUDO_PREFIX}ag --color --smart-case --hidden --literal --ignore '*.min.*' '$@'" + ${SUDO_PREFIX}ag --color \ + --smart-case \ + --hidden \ + --literal \ + --ignore "*.min.*" \ + --file-search-regex ".*\.(ada|asm|awk|bat|c|cbl|cfg|conf|config|cpp|cpy|cs|css|dart|el|erl|ex|f|f90|f95|for|go|gradle|groovy|h|hpp|hrl|htm|html|hx|hxsl|inc|ini|ino|java|js|json|jsx|kt|lib|lua|m|m4|mat|mk|nim|nix|pascal|php|pl|plx|ps1|py|r|rb|rkt|rlib|rs|sc|scala|scss|sh|sml|sql|swift|tcl|template|tex|tpl|ts|tsx|vb|vba|vbs|vhd|vhdl|wren|xml|yaml|yml|zig)" \ + "${@}" \ + 2> /dev/null \ + | awk -v len=$LINE_LENGTH_CUTOFF '{ $0=substr($0, 1, len); print $0 }' + + # Use grep (see findtext function for options explanation) + # Hint: You can use --exclude='/dir/or/file' + else + echo -e "${BRIGHT_CYAN}Search using ${BRIGHT_YELLOW}grep${BRIGHT_CYAN}:${RESET}" + echo "${SUDO_PREFIX}grep --color=always --recursive --ignore-case --binary-files=without-match --with-filename --line-number '$@'" + ${SUDO_PREFIX}grep --color=always --recursive --ignore-case --binary-files=without-match --with-filename --line-number \ + --include=*.ada \ + --include=*.asm \ + --include=*.awk \ + --include=*.bat \ + --include=*.c \ + --include=*.cbl \ + --include=*.cfg \ + --include=*.conf \ + --include=*.config \ + --include=*.cpp \ + --include=*.cpy \ + --include=*.cs \ + --include=*.css \ + --include=*.dart \ + --include=*.el \ + --include=*.erl \ + --include=*.ex \ + --include=*.f90 \ + --include=*.f95 \ + --include=*.f \ + --include=*.for \ + --include=*.go \ + --include=*.gradle \ + --include=*.groovy \ + --include=*.h \ + --include=*.hpp \ + --include=*.hrl \ + --include=*.htm \ + --include=*.html \ + --include=*.hx \ + --include=*.hxsl \ + --include=*.inc \ + --include=*.ini \ + --include=*.ino \ + --include=*.java \ + --include=*.js \ + --include=*.json \ + --include=*.jsx \ + --include=*.kt \ + --include=*.lib \ + --include=*.lua \ + --include=*.m \ + --include=*.m4 \ + --include=*.mat \ + --include=*.mk \ + --include=*.nim \ + --include=*.nix \ + --include=*.pascal \ + --include=*.php \ + --include=*.pl \ + --include=*.plx \ + --include=*.ps1 \ + --include=*.py \ + --include=*.r \ + --include=*.rb \ + --include=*.rkt \ + --include=*.rlib \ + --include=*.rs \ + --include=*.sc \ + --include=*.scala \ + --include=*.scss \ + --include=*.sh \ + --include=*.sml \ + --include=*.sql \ + --include=*.swift \ + --include=*.tcl \ + --include=*.template \ + --include=*.tpl \ + --include=*.tex \ + --include=*.ts \ + --include=*.tsx \ + --include=*.vb \ + --include=*.vba \ + --include=*.vbs \ + --include=*.vhd \ + --include=*.vhdl \ + --include=*.wren \ + --include=*.xml \ + --include=*.yaml \ + --include=*.yml \ + --include=*.zig \ + --exclude='*.min.*' \ + "${@}" \ + | awk -v len=$LINE_LENGTH_CUTOFF '{ $0=substr($0, 1, len); print $0 }' + fi +} + +# Searches for text in all files in the current folder +function findtext() { + # Local constant for maximum line length cut-off + # NOTE: This is necessary for certain files like binaries + local -r LINE_LENGTH_CUTOFF=1000 + + # The prefix to prepend to search commands for elevated permissions + local SUDO_PREFIX="" + + # Check for --sudo flag and remove it from arguments if present + if [[ "$1" == "--sudo" ]]; then + SUDO_PREFIX="sudo " + shift + fi + + # If no search text is specified, show help text + if [[ -z "$1" ]] || [[ "$1" == "--help" ]] || [[ "$1" == "-h" ]]; then + echo -e "${BRIGHT_CYAN}findtext${RESET}: Search for text in all files recursively" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}findtext${RESET} ${BRIGHT_MAGENTA}[${BRIGHT_GREEN}--sudo${BRIGHT_MAGENTA}]${RESET} ${BRIGHT_MAGENTA}<${BRIGHT_YELLOW}pattern${BRIGHT_MAGENTA}>${RESET}" + echo -e "${BRIGHT_WHITE}Options:${RESET}" + echo -e " ${BRIGHT_GREEN}--sudo${RESET} Run with elevated permissions" + echo -e "${BRIGHT_WHITE}Examples:${RESET}" + echo -e " ${BRIGHT_CYAN}findtext${RESET} ${BRIGHT_YELLOW}'example text'${RESET}" + echo -e " ${BRIGHT_CYAN}findtext${RESET} ${BRIGHT_YELLOW}'Hello\\s+world\\.'${RESET} ${BRIGHT_BLUE}# Regex search${RESET}" + echo -e " ${BRIGHT_CYAN}findtext${RESET} ${BRIGHT_GREEN}--sudo${RESET} ${BRIGHT_YELLOW}'todo'${RESET} ${BRIGHT_BLUE}# Search system dirs${RESET}" + return 1 + fi + + # If ripgrep is installed, use that + # Link: https://github.com/BurntSushi/ripgrep + if hascommand --strict rg; then + echo -e "${BRIGHT_CYAN}Search using ${BRIGHT_YELLOW}ripgrep${BRIGHT_CYAN}:${RESET}" + echo "${SUDO_PREFIX}rg --smart-case --no-ignore --hidden --pretty '$@' ." + ${SUDO_PREFIX}rg --smart-case --no-ignore --hidden --pretty "$@" . | \ + awk -v len=$LINE_LENGTH_CUTOFF '{ $0=substr($0, 1, len); print $0 }' + + # If The Silver Searcher is installed, use that + # Link: https://github.com/ggreer/the_silver_searcher + # Hint: You can use --ignore "dir/or/file" + elif hascommand --strict ag; then + echo -e "${BRIGHT_CYAN}Search using ${BRIGHT_YELLOW}The Silver Searcher${BRIGHT_CYAN}:${RESET}" + echo "${SUDO_PREFIX}ag --color --smart-case --hidden --literal '$@'" + ${SUDO_PREFIX}ag --color --smart-case --hidden --literal "$@" 2> /dev/null | \ + awk -v len=$LINE_LENGTH_CUTOFF '{ $0=substr($0, 1, len); print $0 }' + + else # Use grep + # Link: https://www.howtogeek.com/496056/how-to-use-the-grep-command-on-linux/ + # Hint: You can use --exclude='/dir/or/file' + # --ignore-case (-i) : Makes the search case-insensitive + # --binary-files=without-match (-I) : Ignores binary files + # --with-filename (-H) : Displays the filename along with the matching line + # --recursive (-r) : Searches through all subdirectories recursively + # --line-number (-n) : Adds the line number to the output + # Optional: + # --fixed-strings (-F) : Treats the search term as a fixed string (not a regular expression) + # --files-with-matches (-l) : Only outputs the filenames that contain a match (e.g., grep -irl "$@" *) + echo -e "${BRIGHT_CYAN}Search using ${BRIGHT_YELLOW}grep${BRIGHT_CYAN}:${RESET}" + echo "${SUDO_PREFIX}grep --color=always --recursive --ignore-case --binary-files=without-match --with-filename --line-number '$@'" + ${SUDO_PREFIX}grep \ + --color=always \ + --recursive \ + --ignore-case \ + --binary-files=without-match \ + --with-filename \ + --line-number \ + "${@}" \ + | awk -v len=${LINE_LENGTH_CUTOFF} '{ $0=substr($0, 1, len); print $0 }' + fi +} + +# Performs case-insensitive text replacement in a file or directory recursively +function replacetext() { + # Check for --sudo flag and remove it from arguments if present + local SUDO_PREFIX="" + if [[ "$1" == "--sudo" ]]; then + SUDO_PREFIX="sudo " + shift + fi + + # Handle the optional [file_path] parameter + local FILE_PATH="$(command pwd)" + if [[ $# -eq 3 ]]; then + FILE_PATH=$3 + fi + + # If not enough arguments, show help + if [[ $# -lt 2 ]] || [[ "${1}" == "--help" ]] || [[ "${1}" == "-h" ]]; then + echo -e "${BRIGHT_CYAN}replacetext${RESET}: Perform case-insensitive text replacement in files recursively" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}replacetext${RESET} ${BRIGHT_MAGENTA}[${BRIGHT_GREEN}--sudo${BRIGHT_MAGENTA}]${RESET} ${BRIGHT_MAGENTA}<${BRIGHT_YELLOW}find${BRIGHT_MAGENTA}>${RESET} ${BRIGHT_MAGENTA}<${BRIGHT_YELLOW}replace${BRIGHT_MAGENTA}>${RESET} ${BRIGHT_MAGENTA}[${BRIGHT_GREEN}path${BRIGHT_MAGENTA}]${RESET}" + echo -e "${BRIGHT_WHITE}Options:${RESET}" + echo -e " ${BRIGHT_GREEN}--sudo${RESET} Run with elevated permissions" + echo -e "${BRIGHT_WHITE}Examples:${RESET}" + echo -e " ${BRIGHT_CYAN}replacetext${RESET} ${BRIGHT_YELLOW}'foo' 'bar'${RESET} ${BRIGHT_BLUE}# Replace in current dir${RESET}" + echo -e " ${BRIGHT_CYAN}replacetext${RESET} ${BRIGHT_YELLOW}'foo' 'bar' '/path'${RESET} ${BRIGHT_BLUE}# Replace in specific path${RESET}" + echo -e " ${BRIGHT_CYAN}replacetext${RESET} ${BRIGHT_GREEN}--sudo${RESET} ${BRIGHT_YELLOW}'foo' 'bar' '/etc'${RESET} ${BRIGHT_BLUE}# With sudo${RESET}" + return 1 + fi + + # Escape special regex characters and the delimiter for the search pattern + # SC2155: split declaration so sed failure isn't masked by local's exit code + local FIND_TEXT + FIND_TEXT=$(printf '%s' "${1}" | sed 's/[][\\.() *^$+?{|}/-]/\\&/g') + # Escape only characters special in sed replacement text: & \ and the delimiter + local REPLACE_TEXT + REPLACE_TEXT=$(printf '%s' "${2}" | sed 's/[&\\/]/\\&/g') + + # Show safety check and ask for confirmation only for directory operations + if [[ -d "${FILE_PATH}" ]]; then + echo -e "${BRIGHT_GREEN} Find: ${BRIGHT_CYAN}${1}${RESET}" + echo -e "${BRIGHT_GREEN}Replace: ${BRIGHT_CYAN}${2}${RESET}" + echo -e "${BRIGHT_RED}Warning:${BRIGHT_YELLOW} You are about to recursively operate on the directory ${BRIGHT_CYAN}${FILE_PATH}${RESET}" + if ! ask "${BRIGHT_MAGENTA}Are you sure?" "N"; then + echo -e "${BRIGHT_RED}Aborted${RESET}" + return 1 + fi + else + echo -e "${BRIGHT_RED}ERROR: Path not found ${FILE_PATH}${RESET}" + return 1 + fi + + # Execute the find-and-replace operation + echo -e "${BRIGHT_GREEN}Replace: ${BRIGHT_CYAN}find \"${FILE_PATH}\" -type f -exec ${SUDO_PREFIX}sed -i \"s/${FIND_TEXT}/${REPLACE_TEXT}/gi\" {} +${RESET}" + find "${FILE_PATH}" -type f -exec ${SUDO_PREFIX}sed -i "s/${FIND_TEXT}/${REPLACE_TEXT}/gi" {} + +} + +# Enhanced lines function with uppercase variables and improved error messaging +function lines() { + # Check for minimum required arguments + if [[ $# -lt 2 ]] || [[ "${1}" == "--help" ]] || [[ "${1}" == "-h" ]]; then + echo -e "${BRIGHT_CYAN}lines${RESET}: Display specific lines or line ranges from a file" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}lines${RESET} ${BRIGHT_MAGENTA}<${BRIGHT_YELLOW}file${BRIGHT_MAGENTA}>${RESET} ${BRIGHT_MAGENTA}<${BRIGHT_YELLOW}line_number${BRIGHT_MAGENTA}>${RESET} ${BRIGHT_MAGENTA}[${BRIGHT_GREEN}more_lines...${BRIGHT_MAGENTA}]${RESET}" + echo -e "${BRIGHT_WHITE}Examples:${RESET}" + echo -e " ${BRIGHT_CYAN}lines${RESET} ${BRIGHT_YELLOW}file.txt 123${RESET} ${BRIGHT_BLUE}# Single line${RESET}" + echo -e " ${BRIGHT_CYAN}lines${RESET} ${BRIGHT_YELLOW}file.txt 123 456 78${RESET} ${BRIGHT_BLUE}# Multiple lines${RESET}" + echo -e " ${BRIGHT_CYAN}lines${RESET} ${BRIGHT_YELLOW}file.txt 50-100${RESET} ${BRIGHT_BLUE}# Line range${RESET}" + echo -e " ${BRIGHT_CYAN}lines${RESET} ${BRIGHT_YELLOW}file.txt 10 20 30-40${RESET} ${BRIGHT_BLUE}# Mixed${RESET}" + return 1 + fi + + # Assign first argument to FILENAME and shift arguments + local FILENAME="$1" + shift + + # Check if the specified file exists + if [ ! -f "$FILENAME" ]; then + echo -e "${BRIGHT_RED}Error:${RESET} File not found: ${BRIGHT_CYAN}$FILENAME${RESET}" + return 1 + fi + + # Process each argument + while [ $# -gt 0 ]; do + local ARG="$1" + + # Handle line range + if [[ "$ARG" == *-* ]]; then + # Extract start and end lines from the range + local START_LINE=$(echo "$ARG" | cut -d'-' -f1) + local END_LINE=$(echo "$ARG" | cut -d'-' -f2) + + # Display lines within the specified range using 'sed' + sed -n "${START_LINE},${END_LINE}p" "$FILENAME" + + else # Handle individual line number + # Display the specified line using 'sed' + sed -n "${ARG}p" "$FILENAME" + fi + + # Shift to the next argument + shift + done +} + +# Analyzes a given code file to provide metrics and statistics +function analyzecode() { + # Validate input + if [[ $# -eq 0 ]]; then + echo -e "${BRIGHT_WHITE}Usage: ${BRIGHT_CYAN}analyzecode${RESET} ${BRIGHT_GREEN}[filename]${RESET}" + return 1 + fi + + # Check if the file exists and is readable + local FILE="$1" + if [[ ! -f ${FILE} ]] || [[ ! -r ${FILE} ]]; then + echo "Error: File '${FILE}' does not exist or is not readable." + return 1 + fi + + # Initialize counters and variables + local TOTAL_LINES=$(wc -l < "${FILE}") + local NON_COMMENT_LINES=0 + local COMMENT_LINES=0 + local BLANK_LINES=0 + local LONGEST_LINE=0 + local SHORTEST_LINE=999999 + local FILE_SIZE + if [[ "$(uname)" == "Darwin" ]]; then + FILE_SIZE=$(stat -f%z "${FILE}") + else + FILE_SIZE=$(stat -c%s "${FILE}") + fi + local TOTAL_CHARS=0 + + # Process each line + while IFS= read -r LINE; do + local LINE_LENGTH=${#LINE} + [[ $LINE_LENGTH -gt $LONGEST_LINE ]] && LONGEST_LINE=$LINE_LENGTH + + # Exclude blank lines for shortest line and character count + if [[ "$LINE" =~ [^[:space:]] ]]; then + [[ $LINE_LENGTH -lt $SHORTEST_LINE ]] && SHORTEST_LINE=$LINE_LENGTH + ((TOTAL_CHARS+=LINE_LENGTH)) + fi + + # Count blank lines + if [[ "$LINE" =~ ^[[:space:]]*$ ]]; then + ((BLANK_LINES++)) + elif [[ "$LINE" =~ ^[[:space:]]*([#]|\/\/) || "$LINE" =~ \/\* || "$LINE" =~ \*\/ ]]; then + ((COMMENT_LINES++)) + else + ((NON_COMMENT_LINES++)) + fi + done < "$FILE" + + # Handle edge case: if no non-blank lines were found, reset SHORTEST_LINE + [[ ${SHORTEST_LINE} -eq 999999 ]] && SHORTEST_LINE=0 + + # Calculate percentages based on non-blank lines + local NON_BLANK_TOTAL_LINES=$((TOTAL_LINES - BLANK_LINES)) + local AVG_LINE_LENGTH=$((NON_BLANK_TOTAL_LINES ? TOTAL_CHARS / NON_BLANK_TOTAL_LINES : 0)) + local NON_COMMENT_LINE_PERCENT=$((TOTAL_LINES ? NON_COMMENT_LINES * 100 / TOTAL_LINES : 0)) + local COMMENT_LINE_PERCENT=$((TOTAL_LINES ? COMMENT_LINES * 100 / TOTAL_LINES : 0)) + local BLANK_LINE_PERCENT=$((TOTAL_LINES ? BLANK_LINES * 100 / TOTAL_LINES : 0)) + + # Calculate indentation using awk + local METRICS=$(awk ' + BEGIN { + indentTabs = 0; + indentSpaces = 0; + } + { + if (match($0, /^[ \t]+/)) { + indent = substr($0, RSTART, RLENGTH); + indentTabs += gsub(/\t/, "", indent); + indentSpaces += gsub(/ /, "", indent); + } + } + END { + print indentTabs, indentSpaces; + }' "${FILE}") + + local INDENT_TABS=$(echo ${METRICS} | cut -d' ' -f1) + local INDENT_SPACES=$(echo ${METRICS} | cut -d' ' -f2) + + local INDENT_TYPE="Unknown" + [[ ${INDENT_TABS} -gt ${INDENT_SPACES} ]] && INDENT_TYPE="Tabs" + [[ ${INDENT_SPACES} -gt ${INDENT_TABS} ]] && INDENT_TYPE="Spaces" + + # Display results + echo "File Size: ${FILE_SIZE} bytes" + echo "Total Lines: ${TOTAL_LINES}" + echo "Longest Line: ${LONGEST_LINE} characters" + echo "Shortest Line: ${SHORTEST_LINE} characters" + echo "Average Line Length: ${AVG_LINE_LENGTH} characters" + echo "Indentation: ${INDENT_TYPE}" + echo "Non-Comment Lines: ${NON_COMMENT_LINES} (${NON_COMMENT_LINE_PERCENT}%)" + echo "Comment Lines: ${COMMENT_LINES} (${COMMENT_LINE_PERCENT}%)" + echo "Blank Lines: ${BLANK_LINES} (${BLANK_LINE_PERCENT}%)" +} + +# List and sort all function names from code files (with line numbers) +function showfunctions() { + # Check if a filename is provided + if [[ -z "${1}" ]]; then + echo -e "${BRIGHT_WHITE}Usage: ${BRIGHT_CYAN}showfunctions${RESET} ${BRIGHT_GREEN}[filename]${RESET}" + return 1 + fi + + # Perform a grep to find function names and include line numbers + # Use awk for formatting, filtering comments, and cleaning up + # Further filter with awk to exclude names that start with '_' + # Remove leading spaces and sort by function name and then by line number, while also removing duplicates + grep -n -E '(function[[:space:]]+\w+)|(def[[:space:]]+\w+)|(public[[:space:]]+static[[:space:]]+void[[:space:]]+\w+)|(public[[:space:]]+function[[:space:]]+\w+)' "${1}" | \ + awk -F: '$2 !~ /^[[:space:]]*(#|\/\/)/ { gsub(/^[[:space:]]+/, "", $2); print $1 ":\t" $2 }' | \ + sed -E 's/(function|def|public static void|public function)[[:space:]]+//' | \ + awk -F ':\t' '$2 !~ /^_/ {print $1 ":\t" $2}' | \ + sort -k2,2 -k1,1n | \ + uniq -f 1 +} + +# Swap indentations between tabs and spaces +function swapindent() { + # This is the default number of spaces that a tab will be converted to + local DEFAULT_TAB_SPACING=4 + + # If no arguments are provided and no text piped in, or help requested, display help text + if [[ "${1}" == "--help" ]] || [[ "${1}" == "-h" ]] || { [[ $# -eq 0 ]] && [[ -t 0 ]]; }; then + echo -e "${BRIGHT_CYAN}swapindent${RESET}: Swap indentation between tabs and spaces" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}swapindent${RESET} ${BRIGHT_MAGENTA}[${BRIGHT_GREEN}file${BRIGHT_MAGENTA}]${RESET}" + echo -e " command | ${BRIGHT_CYAN}swapindent${RESET}" + echo -e "${BRIGHT_WHITE}Description:${RESET}" + echo -e " ${BRIGHT_BLUE}- If a file is provided, it modifies the file in-place${RESET}" + echo -e " ${BRIGHT_BLUE}- If no file is provided, reads from stdin, writes to stdout${RESET}" + echo -e "${BRIGHT_WHITE}Examples:${RESET}" + echo -e " ${BRIGHT_CYAN}swapindent${RESET} ${BRIGHT_YELLOW}script.sh${RESET} ${BRIGHT_BLUE}# Modify file in-place${RESET}" + echo -e " ${BRIGHT_YELLOW}cat script.sh${RESET} | ${BRIGHT_CYAN}swapindent${RESET} ${BRIGHT_BLUE}# Output to stdout${RESET}" + echo -e " ${BRIGHT_YELLOW}cat script.sh${RESET} | ${BRIGHT_CYAN}swapindent${RESET} | ${BRIGHT_YELLOW}less${RESET} ${BRIGHT_BLUE}# Preview changes${RESET}" + return 0 + fi + + # Declare an array to hold the lines from the input + local LINES=() + + # Declare an array to hold the modified lines + local MODIFIED_LINES=() + + # Variables to hold state for the type of first indent (tab or space) + # and the smallest count of leading spaces in lines + local FIRST_INDENT_TYPE="" + local SMALLEST_SPACE_COUNT=1000 # Initialize to a high value to find the minimum easily + + # Read lines from either a file or standard input into the 'LINES' array + while IFS= read -r LINE; do + LINES+=("${LINE}") # Append line to LINES array + + # Check if the line starts with any kind of whitespace + if [[ "${LINE}" =~ ^[[:space:]] ]]; then + # If the first type of indentation has not yet been determined + if [[ -z "${FIRST_INDENT_TYPE}" ]]; then + # Determine if the first indent in the file is a tab or space + if [[ "${LINE}" =~ ^$'\t' ]]; then + FIRST_INDENT_TYPE="tab" + elif [[ "${LINE}" =~ ^' ' ]]; then + FIRST_INDENT_TYPE="space" + fi + fi + + # If the first indent is a space, count the leading spaces + if [[ "${FIRST_INDENT_TYPE}" == "space" ]]; then + local SPACE_COUNT=$(echo "${LINE}" | sed -E 's/[^ ].*//g' | wc -c) + (( SPACE_COUNT-- )) # Account for the newline character from wc + + # Update the SMALLEST_SPACE_COUNT if this line has fewer leading spaces + [[ ${SPACE_COUNT} -lt ${SMALLEST_SPACE_COUNT} ]] && SMALLEST_SPACE_COUNT=${SPACE_COUNT} + fi + fi + done < <(cat "${1:-/dev/stdin}" && echo) # Append newline so read captures the last line + + # Loop through 'LINES' array to swap and output the indentations + local MODIFIED_LINE + for LINE in "${LINES[@]}"; do + # If the first indent is a tab, convert tabs to spaces + if [[ "${FIRST_INDENT_TYPE}" == "tab" ]]; then + MODIFIED_LINE=$(echo "${LINE}" | sed "s/\t/$(printf "%${DEFAULT_TAB_SPACING}s")/g") + else + # If the first indent is a space, convert spaces to tabs + # Create a string of 'SMALLEST_SPACE_COUNT' number of spaces + local TAB_TO_SPACE_STRING=$(printf "%${SMALLEST_SPACE_COUNT}s") + MODIFIED_LINE=$(echo "${LINE}" | sed "s/${TAB_TO_SPACE_STRING}/\t/g") + fi + + # Append the modified line to MODIFIED_LINES array + MODIFIED_LINES+=("${MODIFIED_LINE}") + done + + # If a filename is provided, write the modified lines back into the file + if [[ -n "$1" ]]; then + printf "%s\n" "${MODIFIED_LINES[@]}" > "$1" + else + # If no filename, print the modified lines to stdout + printf "%s\n" "${MODIFIED_LINES[@]}" + fi +} + +# Copy a file or directory with a progress bar +function cpp() { + # Help text + if [[ "${1}" == "--help" ]] || [[ "${1}" == "-h" ]]; then + echo -e "${BRIGHT_CYAN}cpp${RESET}: Copy files with progress bar (uses rsync)" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}cpp${RESET} ${BRIGHT_MAGENTA}<${BRIGHT_YELLOW}source${BRIGHT_MAGENTA}>${RESET} ${BRIGHT_MAGENTA}<${BRIGHT_YELLOW}destination${BRIGHT_MAGENTA}>${RESET}" + echo -e "${BRIGHT_WHITE}Examples:${RESET}" + echo -e " ${BRIGHT_CYAN}cpp${RESET} ${BRIGHT_YELLOW}largefile.iso${RESET} ${BRIGHT_YELLOW}/mnt/usb/${RESET}" + echo -e " ${BRIGHT_CYAN}cpp${RESET} ${BRIGHT_YELLOW}~/Downloads/video.mp4${RESET} ${BRIGHT_YELLOW}/backup/${RESET}" + return 0 + fi + + # Check the parameters + if [[ -z "${1}" ]]; then + echo "Source is not specified." + return 1 + fi + if [[ -z "${2}" ]]; then + echo "Destination path is not specified." + return 1 + fi + if [[ ! -d "${2}" ]]; then + echo "Destination is not a directory." + return 1 + fi + + # Check if rsync is available + if hascommand --strict rsync; then + # Use rsync with archive mode and overall progress bar + rsync -ah --info=progress2 "${1}" "${2}" + else + # If rsync is not available, use strace with cp to show progress + strace -q -ewrite cp -- "${1}" "${2}" 2>&1 \ + | awk '{ + count += $NF + if (count % 10 == 0) { + percent = int(count / total_size * 100) + printf "%3d%% [", percent + for (i = 0; i <= percent; i++) + printf "=" + printf ">" + for (i = percent; i < 100; i++) + printf " " + printf "]\r" + } + } + END { print "" }' total_size=$([[ "$(uname)" == "Darwin" ]] && stat -f%z "${1}" || stat -c '%s' "${1}") count=0 + fi +} + +# Copy a file and optionally go to the directory +function cpg() { + # Help text + if [[ "${1}" == "--help" ]] || [[ "${1}" == "-h" ]]; then + echo -e "${BRIGHT_CYAN}cpg${RESET}: Copy file and cd to destination directory" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}cpg${RESET} ${BRIGHT_MAGENTA}<${BRIGHT_YELLOW}source${BRIGHT_MAGENTA}>${RESET} ${BRIGHT_MAGENTA}<${BRIGHT_YELLOW}destination${BRIGHT_MAGENTA}>${RESET}" + echo -e "${BRIGHT_WHITE}Examples:${RESET}" + echo -e " ${BRIGHT_CYAN}cpg${RESET} ${BRIGHT_YELLOW}file.txt${RESET} ${BRIGHT_YELLOW}/tmp/${RESET} ${BRIGHT_BLUE}# Copy and cd to /tmp${RESET}" + echo -e " ${BRIGHT_CYAN}cpg${RESET} ${BRIGHT_YELLOW}script.sh${RESET} ${BRIGHT_YELLOW}backup.sh${RESET} ${BRIGHT_BLUE}# Copy with new name${RESET}" + return 0 + fi + + # Check the parameters + if [[ -z "${1}" ]]; then + echo "Source is not specified." + return 1 + fi + if [[ -z "${2}" ]]; then + echo "Destination is not specified." + return 1 + fi + if [[ -d "${2}" ]]; then + # Destination is a directory, copy the file and go to the directory + cp "${1}" "${2}" && cd "${2}" + else + # Destination is not a directory, just copy the file + cp "${1}" "${2}" + fi +} + +# Move a file or directory and optionally go to the directory +function mvg() { + # Help text + if [[ "${1}" == "--help" ]] || [[ "${1}" == "-h" ]]; then + echo -e "${BRIGHT_CYAN}mvg${RESET}: Move file and cd to destination directory" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}mvg${RESET} ${BRIGHT_MAGENTA}<${BRIGHT_YELLOW}source${BRIGHT_MAGENTA}>${RESET} ${BRIGHT_MAGENTA}<${BRIGHT_YELLOW}destination${BRIGHT_MAGENTA}>${RESET}" + echo -e "${BRIGHT_WHITE}Examples:${RESET}" + echo -e " ${BRIGHT_CYAN}mvg${RESET} ${BRIGHT_YELLOW}file.txt${RESET} ${BRIGHT_YELLOW}/tmp/${RESET} ${BRIGHT_BLUE}# Move and cd to /tmp${RESET}" + echo -e " ${BRIGHT_CYAN}mvg${RESET} ${BRIGHT_YELLOW}old.txt${RESET} ${BRIGHT_YELLOW}new.txt${RESET} ${BRIGHT_BLUE}# Rename file${RESET}" + return 0 + fi + + # Check the parameters + if [[ -z "${1}" ]]; then + echo "Source is not specified." + return 1 + fi + if [[ -z "${2}" ]]; then + echo "Destination path is not specified." + return 1 + fi + + if [[ -d "${2}" ]]; then + # Destination is a directory, move the file and go to the directory + mv "${1}" "${2}" && cd "${2}" + else + # Destination is not a directory, just move the file + mv "${1}" "${2}" + fi +} + +# Create and go to the directory +alias md='mkdirg' +function mkdirg() { + # Check if any arguments are provided + if [[ $# -eq 0 ]]; then + # Display usage message and return error code 1 + echo -e "${BRIGHT_WHITE}Usage: ${BRIGHT_CYAN}mkdirg${RESET} ${BRIGHT_GREEN}[directory]${RESET}" + return 1 + fi + + # Verify if the directory already exists + if [ -d "$1" ]; then + # Directory exists, change into it + cd "$1" + else + # Directory doesn't exist, create it and change into it + command mkdir -p "$1" + cd "$1" + fi +} + +# Repeats a given command a specified number of times +function repeat() { + # Check that at least two arguments are provided (number and command) + if [[ $# -lt 2 ]]; then + # Display usage message and return error code 1 + echo -e "${BRIGHT_WHITE}Usage: ${BRIGHT_CYAN}repeat${RESET} ${BRIGHT_GREEN}[count] [command]${RESET}" + return 1 + fi + + # Check that the first argument is a non-negative integer + if ! [[ $1 =~ ^[0-9]+$ ]]; then + # Display error message for invalid count + echo "Error: The count must be a non-negative integer." + return 1 + fi + + # Assign the provided count to maxCount and shift to remove it from arguments + local count maxCount + maxCount=$1; shift + for ((count = 1; count <= maxCount; count++)); do + # Execute the provided command using eval + eval "$@" + done +} + +# Goes up a specified number of directories (i.e. up 4) +# If no argument is provided, it goes up by one directory +function up() { + # Help text + if [[ "${1}" == "--help" ]] || [[ "${1}" == "-h" ]]; then + echo -e "${BRIGHT_CYAN}up${RESET}: Navigate up multiple directory levels" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}up${RESET} ${BRIGHT_MAGENTA}[${BRIGHT_GREEN}levels${BRIGHT_MAGENTA}]${RESET}" + echo -e "${BRIGHT_WHITE}Examples:${RESET}" + echo -e " ${BRIGHT_CYAN}up${RESET} ${BRIGHT_BLUE}# Same as cd ..${RESET}" + echo -e " ${BRIGHT_CYAN}up${RESET} ${BRIGHT_GREEN}2${RESET} ${BRIGHT_BLUE}# Same as cd ../..${RESET}" + echo -e " ${BRIGHT_CYAN}up${RESET} ${BRIGHT_GREEN}5${RESET} ${BRIGHT_BLUE}# Go up 5 directories${RESET}" + return 0 + fi + + # Declare a variable to build the directory path + local DIRECTORY_PATH="" + + # Set the number of levels to go up - default to 1 if not provided + local LEVELS_TO_GO_UP=${1:-1} + + # Loop from 1 to the number of levels to go up + for ((i = 1; i <= LEVELS_TO_GO_UP; i++)); do + # Append "../" to the directory path for each iteration + DIRECTORY_PATH="../${DIRECTORY_PATH}" + done + + # Change to the built directory path + cd "${DIRECTORY_PATH}" +} + +# Returns the last 2 fields of the working directory +function pwdtail() { + pwd | awk -F/ '{nlast = NF -1;print $nlast"/"$NF}' +} + +# Encryption has been changed to gpg from SSL due to security +# Link: https://stackoverflow.com/questions/28247821/openssl-vs-gpg-for-encrypting-off-site-backups +# Install: pkginstall gnupg # gpg2 on SUSE +if hascommand --strict gpg; then + # Set an alias to clear the GPG cache + alias gpgclear='echo RELOADAGENT | gpg-connect-agent' + + ## Uses OpenSSL AES 256bit Cipher Block Chaining Encryption to encrypt a file + function encrypt() { + if [[ -p /dev/stdin ]]; then + # If there is data being piped from stdin + if [[ "${#}" -lt 1 ]]; then + echo -e "${BRIGHT_RED}Error: ${BRIGHT_CYAN}Output file not specified${RESET}" + return 1 + fi + + echo "Encrypting data..." + # equivalents in the gpg.conf file + # s2k-mode 3 + # s2k-count 65011712 + # s2k-digest-algo SHA512 + # s2k-cipher-algo AES256 + gpg --s2k-mode 3 --s2k-count 65011712 --s2k-digest-algo SHA512 --s2k-cipher-algo AES256 --symmetric --output "${1}" + + if [[ $? -eq 0 ]]; then + echo -e "${BRIGHT_GREEN}Data successfully encrypted to ${1}${RESET}" + else + echo -e "${BRIGHT_RED}Encryption failed${RESET}" + return 1 + fi + else + # If no data from stdin, expect a file as argument + if [[ "${#}" -lt 1 ]] || [[ "${1}" = "--help" ]] || [[ "${1}" = "-h" ]]; then + echo -e "${BRIGHT_WHITE}Usage: ${BRIGHT_CYAN}encrypt${RESET} ${BRIGHT_GREEN}[input_file]${RESET} ${BRIGHT_MAGENTA}(the output file will be named [input_file].gpg)${RESET}" + echo -e "${BRIGHT_WHITE} or: ${BRIGHT_YELLOW}command${RESET} | ${BRIGHT_CYAN}encrypt${RESET} ${BRIGHT_GREEN}[output_file]${RESET} ${BRIGHT_MAGENTA}(encrypt data piped from another command)${RESET}" + return + elif [[ ! -r "${1}" ]]; then + echo -e "${BRIGHT_RED}Error: ${BRIGHT_CYAN}File ${BRIGHT_YELLOW}${1}${BRIGHT_CYAN} not found${RESET}" + return 1 + fi + + echo "Encrypting ${1}..." + if gpg --s2k-mode 3 --s2k-count 65011712 --s2k-digest-algo SHA512 --s2k-cipher-algo AES256 --symmetric "${1}"; then + local _encrypted_file="${1}.gpg" + echo -e "${BRIGHT_GREEN}File ${_encrypted_file} successfully encrypted${RESET}" + else + echo -e "${BRIGHT_RED}Encryption failed${RESET}" + return 1 + fi + fi + } + + ## Uses OpenSSL AES 256bit Cipher Block Chaining Encryption to decrypt a file + function decrypt() { + if [[ "${#}" -lt 1 ]] || [[ "${1}" = "--help" ]] || [[ "${1}" = "-h" ]]; then + echo -e "${BRIGHT_WHITE}Usage: ${BRIGHT_CYAN}decrypt${RESET} ${BRIGHT_GREEN}[encrypted_file.gpg]${RESET}" + return + elif [[ ! -r "${1}" ]]; then + echo -e "${BRIGHT_RED}Error: ${BRIGHT_CYAN}File ${BRIGHT_YELLOW}${1}${BRIGHT_CYAN} not found${RESET}" + return 1 + fi + + echo "Decrypting ${1}..." + + local _DECRYPTED_FILE="${1%.gpg}" # Remove '.gpg' extension from the input file name + + if gpg --output "${_DECRYPTED_FILE}" --decrypt "${1}"; then + echo -e "${BRIGHT_GREEN}File successfully decrypted to ${_DECRYPTED_FILE}${RESET}" + else + echo -e "${BRIGHT_RED}Error decrypting ${BRIGHT_YELLOW}${1}${RESET}" + return 1 + fi + } +fi + +# Print a list of colors +function colors() { + # Help text + if [[ "${1}" == "--help" ]] || [[ "${1}" == "-h" ]]; then + echo -e "${BRIGHT_CYAN}colors${RESET}: Display basic 8-color terminal palette" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}colors${RESET}" + echo -e "${BRIGHT_WHITE}See also:${RESET} ${BRIGHT_GREEN}colors256${RESET}, ${BRIGHT_GREEN}colors24bit${RESET}" + return 0 + fi + + # Define variables for foreground, background, values, and sequence + local foreground background values sequence + + # Print introductory information about color escapes and styles + printf "Color escapes are %s\n" '\033[${value};...;${value}m' + printf "Values 30..37 are \033[33mforeground colors\033[m\n" + printf "Values 40..47 are \033[43mbackground colors\033[m\n" + printf "Value 1 gives a \033[1mbold-faced look\033[m\n\n" + + # Iterate through foreground colors (values 30 to 37) + for foreground in {30..37}; do + # Iterate through background colors (values 40 to 47) + for background in {40..47}; do + # Extract color names from numerical values + # Use separate variables to avoid clobbering the loop iterators + local FG_VAL=${foreground#37} # white + local BG_VAL=${background#40} # black + + # Construct values string for color escape sequences + values="${FG_VAL:+$FG_VAL;}${BG_VAL}" + values=${values%%;} + + # Construct escape sequence for color styling + sequence="${values:+\033[${values}m}" + + # Print formatted color examples + printf " %-9s" "${sequence:-(default)}" + printf " ${sequence}TEXT\033[m" + printf " \033[${values:+${values+$values;}}1mBOLD\033[m" + done + echo; echo + done +} + +# Print a list of 256 colors +function colors256() { + # Help text + if [[ "${1}" == "--help" ]] || [[ "${1}" == "-h" ]]; then + echo -e "${BRIGHT_CYAN}colors256${RESET}: Display 256-color terminal palette" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}colors256${RESET}" + echo -e "${BRIGHT_WHITE}See also:${RESET} ${BRIGHT_GREEN}colors${RESET}, ${BRIGHT_GREEN}colors24bit${RESET}" + return 0 + fi + + # Define the number of colors per line + colors_per_line=8 + + for i in {0..255}; do + printf "\x1b[38;5;${i}mcolor%-5d" "$i" + + # Check if the current color is the last in the line + if (( (i + 1) % colors_per_line == 0 )); then + echo # Move to the next line + fi + done + # Add a final newline if needed + if ((255 % colors_per_line != 0)); then + echo + fi +} + +# Test for 24bit true color in the terminal +function colors24bit() { + # Help text + if [[ "${1}" == "--help" ]] || [[ "${1}" == "-h" ]]; then + echo -e "${BRIGHT_CYAN}colors24bit${RESET}: Test terminal 24-bit true color support" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}colors24bit${RESET}" + echo -e "${BRIGHT_WHITE}Description:${RESET}" + echo -e " ${BRIGHT_BLUE}Displays color gradients - smooth = true color supported${RESET}" + echo -e "${BRIGHT_WHITE}See also:${RESET} ${BRIGHT_GREEN}colors${RESET}, ${BRIGHT_GREEN}colors256${RESET}" + return 0 + fi + + echo 'If the gradients are smooth, you are displaying 24bit true color.' + awk 'BEGIN{ + # Generate a long string of characters for testing + s = "1234567890"; + s = s s s s s s s s s s s s s s s s s s s s s s s s; # Extended string + len = length(s); # Length of the string + + # Generate and display color gradients + for (colnum = 0; colnum < 256; colnum++) { + r = 255 - (colnum * 255 / 255); + g = (colnum * 510 / 255); + b = (colnum * 255 / 255); + if (g > 255) g = 510 - g; + + # Set background and foreground colors using ANSI escape sequences + printf "\033[48;2;%d;%d;%dm", r, g, b; # Background color + printf "\033[38;2;%d;%d;%dm", 255 - r, 255 - g, 255 - b; # Foreground color + + # Display a character with the defined colors and reset formatting + printf "%s\033[0m", substr(s, (colnum % len) + 1, 1); + } + printf "\n"; # Move to the next line after printing colors + }' +} + +# Prints random height bars across the width of the screen +# (great with lolcat application on new terminal windows) +function sparkbars() { + columns=$(tput cols) + chars=▁▂▃▄▅▆▇█ + for ((i = 1; i <= $columns; i++)) + do + echo -n "${chars:RANDOM%${#chars}:1}" + done + echo +} + +# View Apache logs +function apachelog() { + # Help text + if [[ "${1}" == "--help" ]] || [[ "${1}" == "-h" ]]; then + echo -e "${BRIGHT_CYAN}apachelog${RESET}: View Apache web server logs" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}apachelog${RESET}" + echo -e "${BRIGHT_WHITE}Description:${RESET}" + echo -e " ${BRIGHT_BLUE}Auto-detects /var/log/httpd or /var/log/apache2${RESET}" + return 0 + fi + + if [ -d /var/log/httpd ]; then + sudo \ls -Ah /var/log/httpd && logview "/var/log/httpd/*_log" + else + sudo \ls -Ah /var/log/apache2 && logview "/var/log/apache2/*.log" + fi +} + +# Auto-find and edit the Apache configuration +function apacheconfig() { + # Help text + if [[ "${1}" == "--help" ]] || [[ "${1}" == "-h" ]]; then + echo -e "${BRIGHT_CYAN}apacheconfig${RESET}: Edit Apache configuration file" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}apacheconfig${RESET} ${BRIGHT_MAGENTA}[${BRIGHT_GREEN}config_path${BRIGHT_MAGENTA}]${RESET}" + echo -e "${BRIGHT_WHITE}Description:${RESET}" + echo -e " ${BRIGHT_BLUE}Auto-detects httpd.conf or apache2.conf location${RESET}" + echo -e "${BRIGHT_WHITE}Examples:${RESET}" + echo -e " ${BRIGHT_CYAN}apacheconfig${RESET} ${BRIGHT_BLUE}# Auto-find config${RESET}" + echo -e " ${BRIGHT_CYAN}apacheconfig${RESET} ${BRIGHT_GREEN}/etc/httpd/conf/httpd.conf${RESET} ${BRIGHT_BLUE}# Specific path${RESET}" + return 0 + fi + + if hascommand --strict httpd || hascommand --strict apache2 || hascommand --strict apachectl; then + # Define an array with common paths + declare -a paths=( + "/etc/httpd/conf/httpd.conf" + "/etc/httpd/httpd.conf" + "/etc/apache2/apache2.conf" + "/usr/local/apache2/apache2.conf" + "/usr/local/etc/httpd/httpd.conf" + ) + + # Check if a custom path is provided + [[ -n "$1" ]] && paths=("$1") + for path in "${paths[@]}"; do + if [[ -f $path ]]; then + edit "$path" + return 0 + fi + done + + echo "Error: Apache config file could not be found." + echo "Searching for possible locations:" + httpd -V 2> /dev/null || apachectl -V 2> /dev/null || apache2 -V + else + echo "Apache is not installed." + fi +} + +# Find the Apache service and restart/start it +function apacherestart() { + # Help text + if [[ "${1}" == "--help" ]] || [[ "${1}" == "-h" ]]; then + echo -e "${BRIGHT_CYAN}apacherestart${RESET}: Test config and restart Apache service" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}apacherestart${RESET}" + echo -e "${BRIGHT_WHITE}Description:${RESET}" + echo -e " ${BRIGHT_BLUE}Tests configuration before restarting${RESET}" + echo -e " ${BRIGHT_BLUE}Auto-detects apache2/httpd service name${RESET}" + return 0 + fi + + # Determine Apache config test command + local apache_cmd + if hascommand --strict apache2ctl; then + apache_cmd="apache2ctl" + elif hascommand --strict httpd; then + apache_cmd="httpd" + else + echo -e "${BRIGHT_RED}Error: Could not find Apache executable (apache2ctl or httpd)${RESET}" + return 1 + fi + + # Test configuration FIRST + echo -e "${BRIGHT_CYAN}Testing Apache configuration...${RESET}" + if ! sudo $apache_cmd -t; then + echo -e "${BRIGHT_RED}✗ Configuration test failed - cannot restart Apache with invalid config${RESET}" + echo -e "${BRIGHT_CYAN}Fix the configuration errors and try again${RESET}" + return 1 + fi + echo -e "${BRIGHT_GREEN}✓ Configuration is valid${RESET}" + + # Determine service name by trying common names + local apache_service + if hascommand --strict systemctl; then + for service in apache2 httpd apache; do + if systemctl list-unit-files | grep -q "^$service.service"; then + apache_service="$service" + break + fi + done + + if [[ -z "$apache_service" ]]; then + echo -e "${BRIGHT_RED}Error: Could not find Apache service (tried apache2, httpd, apache)${RESET}" + return 1 + fi + + echo -e "${BRIGHT_CYAN}Restarting Apache ($apache_service) via systemctl...${RESET}" + if sudo systemctl restart $apache_service; then + echo -e "${BRIGHT_GREEN}✓ Apache restarted successfully${RESET}" + + if sudo systemctl is-active $apache_service &>/dev/null; then + echo -e "${BRIGHT_GREEN}✓ Apache is running${RESET}" + else + echo -e "${BRIGHT_RED}✗ Apache failed to start - check logs: sudo journalctl -u $apache_service${RESET}" + return 1 + fi + else + echo -e "${BRIGHT_RED}✗ Failed to restart Apache${RESET}" + return 1 + fi + # [rest of init.d and service logic similar to before, but with service name detection] + fi +} + +# Auto-find and edit the Nginx configuration +function ngconfig() { + # Help text + if [[ "${1}" == "--help" ]] || [[ "${1}" == "-h" ]]; then + echo -e "${BRIGHT_CYAN}ngconfig${RESET}: Edit Nginx configuration file" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}ngconfig${RESET} ${BRIGHT_MAGENTA}[${BRIGHT_GREEN}config_path${BRIGHT_MAGENTA}]${RESET}" + echo -e "${BRIGHT_WHITE}Description:${RESET}" + echo -e " ${BRIGHT_BLUE}Auto-detects nginx.conf location${RESET}" + echo -e "${BRIGHT_WHITE}Examples:${RESET}" + echo -e " ${BRIGHT_CYAN}ngconfig${RESET} ${BRIGHT_BLUE}# Auto-find config${RESET}" + echo -e " ${BRIGHT_CYAN}ngconfig${RESET} ${BRIGHT_GREEN}/etc/nginx/nginx.conf${RESET} ${BRIGHT_BLUE}# Specific path${RESET}" + return 0 + fi + + if hascommand --strict nginx; then + # Define an array with common paths + declare -a paths=( + "/etc/nginx/nginx.conf" + "/usr/local/nginx/conf/nginx.conf" + "/usr/local/etc/nginx/nginx.conf" + ) + + # Check if a custom path is provided + [[ -n "$1" ]] && paths=("$1") + + for path in "${paths[@]}"; do + if [[ -f $path ]]; then + edit "$path" + return 0 + fi + done + + echo "Error: Nginx config file could not be found." + echo "Please specify the location manually, or check your Nginx installation." + else + echo "Nginx is not installed." + fi +} + +# Find the Nginx service and restart/start it +function ngrestart() { + # Help text + if [[ "${1}" == "--help" ]] || [[ "${1}" == "-h" ]]; then + echo -e "${BRIGHT_CYAN}ngrestart${RESET}: Test config and restart Nginx service" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}ngrestart${RESET}" + echo -e "${BRIGHT_WHITE}Description:${RESET}" + echo -e " ${BRIGHT_BLUE}Tests configuration before restarting${RESET}" + return 0 + fi + + # Test configuration FIRST before attempting restart + echo -e "${BRIGHT_CYAN}Testing nginx configuration...${RESET}" + if ! sudo nginx -t; then + echo -e "${BRIGHT_RED}✗ Configuration test failed - cannot restart nginx with invalid config${RESET}" + echo -e "${BRIGHT_CYAN}Fix the configuration errors and try again${RESET}" + return 1 + fi + echo -e "${BRIGHT_GREEN}✓ Configuration is valid${RESET}" + + # If we are using Systemd... + if hascommand --strict systemctl; then + # Check if Nginx executable is available + if hascommand --strict nginx; then + echo -e "${BRIGHT_CYAN}Restarting nginx via systemctl...${RESET}" + if sudo systemctl restart nginx; then + echo -e "${BRIGHT_GREEN}✓ Nginx restarted successfully${RESET}" + + # Check if service is actually running + if sudo systemctl is-active nginx &>/dev/null; then + echo -e "${BRIGHT_GREEN}✓ Nginx is running${RESET}" + else + echo -e "${BRIGHT_RED}✗ Nginx failed to start - check logs: sudo journalctl -u nginx${RESET}" + return 1 + fi + else + echo -e "${BRIGHT_RED}✗ Failed to restart nginx${RESET}" + return 1 + fi + else + echo -e "${BRIGHT_RED}Error: Could not find nginx executable${RESET}" + return 1 + fi + # If using init.d... + elif [[ -d /etc/init.d ]] && [[ -f /etc/init.d/nginx ]]; then + echo -e "${BRIGHT_CYAN}Restarting nginx via init.d...${RESET}" + if sudo /etc/init.d/nginx restart; then + echo -e "${BRIGHT_GREEN}✓ Nginx restarted successfully${RESET}" + else + echo -e "${BRIGHT_RED}✗ Failed to restart nginx${RESET}" + return 1 + fi + # Other systems including OpenRC... + elif hascommand --strict service; then + # Check if Nginx executable is available + if hascommand --strict nginx; then + echo -e "${BRIGHT_CYAN}Restarting nginx via service command...${RESET}" + if sudo service nginx restart; then + echo -e "${BRIGHT_GREEN}✓ Nginx restarted successfully${RESET}" + else + echo -e "${BRIGHT_RED}✗ Failed to restart nginx${RESET}" + return 1 + fi + else + echo -e "${BRIGHT_RED}Error: Could not find nginx executable${RESET}" + return 1 + fi + else # Unknown + echo -e "${BRIGHT_RED}Error: Could not find service controller (systemctl, service, or init.d)${RESET}" + return 1 + fi +} + +# Check the syntax of a PHP file for errors +function phpcheck() { + if [[ $# -eq 0 ]] || [[ "${1}" == "--help" ]] || [[ "${1}" == "-h" ]]; then + echo -e "${BRIGHT_CYAN}phpcheck${RESET}: Validate PHP syntax without executing the file" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}phpcheck${RESET} ${BRIGHT_MAGENTA}<${BRIGHT_YELLOW}file.php${BRIGHT_MAGENTA}>${RESET}" + echo -e "${BRIGHT_WHITE}Examples:${RESET}" + echo -e " ${BRIGHT_CYAN}phpcheck${RESET} ${BRIGHT_YELLOW}index.php${RESET}" + return 2 + fi + echo -e "${BRIGHT_RED}[${BRIGHT_CYAN}PHP Check${BRIGHT_RED}]${BRIGHT_YELLOW}->${BRIGHT_GREEN}${1}${RESET}" + php -l "${1}" +} + +# Auto-find and edit the PHP configuration file +function phpconfig() { + # Help text + if [[ "${1}" == "--help" ]] || [[ "${1}" == "-h" ]]; then + echo -e "${BRIGHT_CYAN}phpconfig${RESET}: Edit PHP configuration file (php.ini)" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}phpconfig${RESET}" + echo -e "${BRIGHT_WHITE}Description:${RESET}" + echo -e " ${BRIGHT_BLUE}Auto-detects php.ini location via PHP itself${RESET}" + return 0 + fi + + if hascommand --strict php; then + local _php_ini_file=$(php -r 'echo php_ini_loaded_file();') + if [[ -f "${_php_ini_file}" ]]; then + echo "Found: ${_php_ini_file}" + edit "${_php_ini_file}" + elif [[ -f /etc/php.ini ]]; then + echo "Found: /etc/php.ini" + edit /etc/php.ini + elif [[ -f /etc/php/php.ini ]]; then + echo "Found: /etc/php/php.ini" + edit /etc/php/php.ini + else + echo "Error: php.ini file could not be found automatically." + echo "Searching for possible locations:" + # sudo updatedb && locate php.ini + php --ini + fi + else + echo "PHP is not installed." + fi +} + +# Auto-find and edit the MySQL configuration file +function mysqlconfig() { + # Help text + if [[ "${1}" == "--help" ]] || [[ "${1}" == "-h" ]]; then + echo -e "${BRIGHT_CYAN}mysqlconfig${RESET}: Edit MySQL configuration file (my.cnf)" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}mysqlconfig${RESET}" + echo -e "${BRIGHT_WHITE}Description:${RESET}" + echo -e " ${BRIGHT_BLUE}Auto-detects my.cnf location${RESET}" + return 0 + fi + + if hascommand --strict mysqld; then + if [[ -f /etc/my.cnf ]]; then + edit /etc/my.cnf + elif [[ -f /etc/mysql/my.cnf ]]; then + edit /etc/mysql/my.cnf + elif [[ -f /usr/local/etc/my.cnf ]]; then + edit /usr/local/etc/my.cnf + elif [[ -f /usr/bin/mysql/my.cnf ]]; then + edit /usr/bin/mysql/my.cnf + elif [[ -f "${HOME}/my.cnf" ]]; then + edit "${HOME}/my.cnf" + else + echo "Error: my.cnf file could not be found automatically." + echo "Searching for possible locations:" + # sudo updatedb && locate my.cnf + mysqld --verbose --help | grep -A 1 "Default options" + fi + else + echo "MySQL is not installed." + fi +} + +# Determine if a system reboot is required +function checkreboot() { + # Help text + if [[ "${1}" == "--help" ]] || [[ "${1}" == "-h" ]]; then + echo -e "${BRIGHT_CYAN}checkreboot${RESET}: Check if a system reboot is required" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}checkreboot${RESET}" + echo -e "${BRIGHT_WHITE}Description:${RESET}" + echo -e " ${BRIGHT_BLUE}Detects kernel updates on Arch, RHEL, Debian, SUSE${RESET}" + return 0 + fi + + # Initialize a variable to indicate if a reboot is required + local REBOOT_REQUIRED=0 + + # Check if pacman exists (indicative of an Arch-based system) + if command -v pacman &> /dev/null; then + + # Determine the running kernel's package name from uname -r + # uname -r returns e.g. "6.18.8-zen2-1-zen" with a trailing flavor suffix + local RUNNING_UNAME + RUNNING_UNAME=$(uname -r) + local KERNEL_PKG="linux" + case "${RUNNING_UNAME}" in + *-lts*) KERNEL_PKG="linux-lts" ;; + *-zen*) KERNEL_PKG="linux-zen" ;; + *-hardened*) KERNEL_PKG="linux-hardened" ;; + esac + + # Get the installed version of that specific kernel package + local INSTALLED_VER + INSTALLED_VER=$(pacman -Q "${KERNEL_PKG}" 2>/dev/null | awk '{print $2}') + + if [[ -n "${INSTALLED_VER}" ]]; then + # Normalize both: replace all dashes with dots for comparison + local ACTIVE_KERNEL=${RUNNING_UNAME//-/.} + local CURRENT_KERNEL=${INSTALLED_VER//-/.} + + # uname -r has a trailing flavor suffix (e.g. ".zen", ".lts") + # that pacman's version string doesn't include. + # Check if active starts with installed version to handle this. + if [[ "${ACTIVE_KERNEL}" != "${CURRENT_KERNEL}"* ]]; then + echo -e "${BRIGHT_RED}->${RESET} ${BRIGHT_CYAN}Arch Linux${RESET}: ${BRIGHT_YELLOW}Running kernel (${ACTIVE_KERNEL}) does not match installed ${KERNEL_PKG} (${CURRENT_KERNEL}).${RESET}" + REBOOT_REQUIRED=1 + fi + fi + + # Use hascommand to check if rpm exists (indicative of a RedHat-based system like CentOS) + elif hascommand --strict rpm; then + if ! rpm -q kernel | grep -q $(uname -r); then + echo -e "${BRIGHT_RED}->${RESET} ${BRIGHT_CYAN}Red Hat Linux${RESET}: ${BRIGHT_YELLOW}Kernel update detected. A reboot is required.${RESET}" + REBOOT_REQUIRED=1 + fi + + # Check for the existence of /var/run/reboot-required (indicative of Ubuntu/Debian) + elif [[ -f /var/run/reboot-required ]]; then + # If the file exists, a reboot is required + echo -e "${BRIGHT_RED}->${RESET} ${BRIGHT_CYAN}Ubuntu/Debian${RESET}: ${BRIGHT_YELLOW}A reboot is required.${RESET}" + REBOOT_REQUIRED=1 + + # Check if zypper exists (indicative of a SUSE-based system) + elif command -v zypper &> /dev/null; then + if zypper ps -s | grep -q 'yes'; then + echo -e "${BRIGHT_RED}->${RESET} ${BRIGHT_CYAN}SUSE${RESET}: ${BRIGHT_YELLOW}Kernel or service update detected. A reboot is required.${RESET}" + REBOOT_REQUIRED=1 + fi + fi + + # General: Check for deleted libraries still in use + local LIBRARIES=$(lsof -n +c 0 2> /dev/null | grep 'DEL.*lib' | awk '{print $1 ": " $NF}' | sort -u) + if [[ -n ${LIBRARIES} ]]; then + echo -e "${BRIGHT_RED}->${RESET} ${BRIGHT_CYAN}General${RESET}: ${BRIGHT_YELLOW}The following libraries require a reboot:${RESET}" + echo "${LIBRARIES}" + REBOOT_REQUIRED=1 + fi + + # Final message and exit code + if [[ ${REBOOT_REQUIRED} -eq 0 ]]; then + echo -e "${BRIGHT_RED}->${RESET} ${BRIGHT_CYAN}Results${RESET}: ${BRIGHT_GREEN}No reboot is required.${RESET}" + return 0 + else + echo -e "${BRIGHT_RED}->${RESET} ${BRIGHT_CYAN}Results${RESET}: ${BRIGHT_YELLOW}A reboot is required.${RESET}" + return 1 + fi +} + +# Interactively create, configure, and test a new Linux user +function createuser() { + # Help text + if [[ "${1}" == "--help" ]] || [[ "${1}" == "-h" ]]; then + echo -e "${BRIGHT_CYAN}createuser${RESET}: Create a new user account interactively" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}createuser${RESET} ${BRIGHT_MAGENTA}[${BRIGHT_GREEN}username${BRIGHT_MAGENTA}]${RESET}" + echo -e "${BRIGHT_WHITE}Description:${RESET}" + echo -e " ${BRIGHT_BLUE}Creates home directory, sets password, optionally grants sudo${RESET}" + echo -e "${BRIGHT_WHITE}Examples:${RESET}" + echo -e " ${BRIGHT_CYAN}createuser${RESET} ${BRIGHT_BLUE}# Interactive prompts${RESET}" + echo -e " ${BRIGHT_CYAN}createuser${RESET} ${BRIGHT_GREEN}newuser${RESET} ${BRIGHT_BLUE}# Specify username${RESET}" + return 0 + fi + + local username + + # Check if the user can execute sudo commands + if ! sudo -v; then + echo -e "${BRIGHT_RED}Error:${RESET} You do not have sufficient permissions to run this script with necessary privileges" + return 1 + fi + + # Check if a username was passed as a parameter + if [ "$#" -eq 1 ]; then + username="$1" + else + read -r -p $'Enter the username for the new user: ' username + fi + + # Check if the user already exists + if [[ -z "${username}" ]]; then + echo -e "${BRIGHT_RED}User cannot be blank. Aborting.${RESET}" + return 1 + elif id "${username}" &>/dev/null; then + echo -e "${BRIGHT_RED}User ${username} already exists. Aborting.${RESET}" + return 1 + fi + + # Confirm if the user should be created with a home directory + if ask "${BRIGHT_GREEN}Create a new user with a home folder?${RESET}" N; then + sudo useradd -m "${username}" + else + echo -e "${BRIGHT_RED}User creation aborted${RESET}" + return 1 + fi + + # Set the user's password + echo -e "${BRIGHT_YELLOW}\nSet the user's password:${RESET}" + sudo passwd "${username}" + + # Ask if the user should change their password upon next login + if ask "${BRIGHT_GREEN}Force user to change password on next login?${RESET}" N; then + sudo passwd -e "${username}" + else + echo -e "${BRIGHT_YELLOW}No change password enforced${RESET}" + fi + + # Ask if the user should have root (sudo) access + if ask "${BRIGHT_MAGENTA}⚠️ Give user root access? ⚠️${RESET}" N; then + sudo usermod -a -G sudo "${username}" + else + echo -e "${BRIGHT_YELLOW}No root access granted${RESET}" + fi + + # Change the user's login shell to bash + echo -e "${BRIGHT_CYAN}\nChange user’s login shell to bash${RESET}" + sudo usermod --shell /bin/bash "${username}" + + # Verify the user's settings + echo -e "${BRIGHT_YELLOW}\nVerifying user settings:${RESET}" + sudo grep "${username}" /etc/passwd + + # Ask if you should copy over the local .bashrc to the new user + if ask "${BRIGHT_GREEN}Copy over your local .bashrc?${RESET}" N; then + sudo cp ~/.bashrc /home/"${username}"/ + sudo chown "${username}":"${username}" /home/"${username}"/.bashrc + sudo chmod 644 /home/"${username}"/.bashrc + else + echo -e "${BRIGHT_YELLOW}No .bashrc copy${RESET}" + fi + + # Test login with the new user + if ask "${BRIGHT_GREEN}⚠️ Test a login as this user? ⚠️${RESET}" N; then + echo -e "${BRIGHT_CYAN}\nTesting: Logging in as ${username}${RESET}" + sudo su - "${username}" + fi +} + +# Remove a user from the system +alias deleteuser='sudo userdel' +function wipeuser() { + # Help text + if [[ "${1}" == "--help" ]] || [[ "${1}" == "-h" ]]; then + echo -e "${BRIGHT_CYAN}wipeuser${RESET}: Completely remove a user and their home directory" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}wipeuser${RESET} ${BRIGHT_MAGENTA}[${BRIGHT_GREEN}username${BRIGHT_MAGENTA}]${RESET}" + echo -e "${BRIGHT_WHITE}Description:${RESET}" + echo -e " ${BRIGHT_BLUE}Kills processes, removes home dir, removes from groups${RESET}" + echo -e " ${BRIGHT_BLUE}If no username given, shows interactive menu${RESET}" + echo -e "${BRIGHT_WHITE}Examples:${RESET}" + echo -e " ${BRIGHT_CYAN}wipeuser${RESET} ${BRIGHT_BLUE}# Select from menu${RESET}" + echo -e " ${BRIGHT_CYAN}wipeuser${RESET} ${BRIGHT_GREEN}olduser${RESET} ${BRIGHT_BLUE}# Remove specific user${RESET}" + return 0 + fi + + local USERNAME="$1" + + # Check if the user can execute sudo commands + if ! sudo -v; then + echo -e "${BRIGHT_RED}Error:${RESET} You do not have sufficient permissions to run this script with necessary privileges" + return 1 + fi + + # If username is not provided, get the list of users and use createmenu + if [[ -z "${USERNAME}" ]]; then + echo -e "${BRIGHT_CYAN}Select a user to delete:${RESET}" + USERNAME=$(sudo awk -F: '$3 >= 1000 && $3 < 65534 {print $1}' /etc/passwd | createmenu) + fi + + # If username is still empty (e.g. if the user cancels the menu selection), exit + if [[ -z "${USERNAME}" ]]; then + echo -e "${BRIGHT_RED}No user selected. Aborting.${RESET}" + return 1 + + # Check against this being the current user + elif [[ "${USERNAME}" == "${USER}" ]]; then + echo -e "${BRIGHT_RED}You cannot remove the currently logged-in user. Aborting.${RESET}" + return 1 + fi + + # Check if the user exists + if id "${USERNAME}" &>/dev/null; then + + # Confirm deletion + if ask "${BRIGHT_RED}⚠️ Are you sure you want to delete user ${USERNAME} and all their data? ⚠️ This action cannot be undone! ⚠️${RESET}" N; then + + # Kill all processes by the user + sudo pkill -U "${USERNAME}" + + # Remove the user and their home directory + sudo userdel -rf "${USERNAME}" + + # Remove the user from any additional groups + sudo delgroup "${USERNAME}" &>/dev/null + + echo -e "${BRIGHT_GREEN}User ${USERNAME} and their home directory have been deleted.${RESET}" + else + echo -e "${BRIGHT_YELLOW}User deletion aborted.${RESET}" + fi + else + echo -e "${BRIGHT_RED}User ${USERNAME} does not exist.${RESET}" + fi +} + +# A full chmod calculator on command line (type chmodcalc for usage and examples) +function chmodcalc() { + # Show help if requested + if [[ "${1}" == "--help" ]] || [[ "${1}" == "-h" ]]; then + echo -e "${BRIGHT_CYAN}chmodcalc${RESET}: Calculate and display chmod permissions" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}chmodcalc${RESET} ${BRIGHT_MAGENTA}<${BRIGHT_YELLOW}octal${BRIGHT_MAGENTA}>${RESET}" + echo -e " ${BRIGHT_CYAN}chmodcalc${RESET} ${BRIGHT_MAGENTA}<${BRIGHT_YELLOW}owner${BRIGHT_MAGENTA}>${RESET} ${BRIGHT_MAGENTA}<${BRIGHT_YELLOW}group${BRIGHT_MAGENTA}>${RESET} ${BRIGHT_MAGENTA}<${BRIGHT_YELLOW}other${BRIGHT_MAGENTA}>${RESET}" + echo -e "${BRIGHT_WHITE}Examples:${RESET}" + echo -e " ${BRIGHT_CYAN}chmodcalc${RESET} ${BRIGHT_YELLOW}755${RESET} ${BRIGHT_BLUE}# Show rwx for 755${RESET}" + echo -e " ${BRIGHT_CYAN}chmodcalc${RESET} ${BRIGHT_YELLOW}rwx rw r${RESET} ${BRIGHT_BLUE}# Convert to octal (754)${RESET}" + echo -e "${BRIGHT_WHITE}Octal values:${RESET} read=${BRIGHT_CYAN}4${RESET}, write=${BRIGHT_CYAN}2${RESET}, execute=${BRIGHT_CYAN}1${RESET}" + return 0 + fi + + # Validate the number of arguments + if [[ "$#" -eq 1 ]]; then + # Validate the length of the argument (accept 1-4 digit octals) + if [[ "${#1}" -gt 4 ]] || [[ "${#1}" -lt 1 ]]; then + echo -e "${BRIGHT_RED}Error: ${BRIGHT_CYAN}Invalid octal (use 1-4 digits).${RESET}" + return 128 + fi + + # Handle 4-digit octals: extract the special bit prefix + local SPECIAL_BIT="" + local TEXT="$1" + if [[ "${#TEXT}" -eq 4 ]]; then + SPECIAL_BIT="${TEXT:0:1}" + TEXT="${TEXT:1}" + fi + local -a PART=() + local EXAMPLE + local INDEX=0 + + while (( INDEX++ < ${#TEXT} )); do + # Extract individual octal digit + local CHAR="${TEXT:INDEX-1:1}" + + # Map octal digit to permissions + case ${CHAR} in + 0) PART[${INDEX}]="---" ;; + 1) PART[${INDEX}]="--x" ;; + 2) PART[${INDEX}]="-w-" ;; + 3) PART[${INDEX}]="-wx" ;; + 4) PART[${INDEX}]="r--" ;; + 5) PART[${INDEX}]="r-x" ;; + 6) PART[${INDEX}]="rw-" ;; + 7) PART[${INDEX}]="rwx" ;; + *) + echo -e "${BRIGHT_RED}Error: ${BRIGHT_CYAN}Invalid octal digit at position ${BRIGHT_YELLOW}${INDEX}${RESET}" + return 128 + ;; + esac + + # Create example representation + EXAMPLE[${INDEX}]="${PART[${INDEX}]//-}" + done + + # Display formatted output and examples + echo -e "${BRIGHT_GREEN}${PART[1]}${RESET} ${BRIGHT_YELLOW}${PART[2]}${RESET} ${BRIGHT_RED}${PART[3]}${RESET}" + + # Display special bit info if a 4-digit octal was provided + if [[ -n "${SPECIAL_BIT}" ]] && [[ "${SPECIAL_BIT}" -ne 0 ]]; then + local SPECIAL_DESC="" + (( SPECIAL_BIT & 4 )) && SPECIAL_DESC+="setuid " + (( SPECIAL_BIT & 2 )) && SPECIAL_DESC+="setgid " + (( SPECIAL_BIT & 1 )) && SPECIAL_DESC+="sticky " + echo -e "${BRIGHT_MAGENTA}Special bit (${SPECIAL_BIT}):${RESET} ${SPECIAL_DESC}" + fi + + echo -e "Examples:" + local FULL_OCTAL="${SPECIAL_BIT}${TEXT}" + echo -e "${BRIGHT_CYAN}chmod${RESET} ${BRIGHT_CYAN}-R${RESET} ${BRIGHT_MAGENTA}${FULL_OCTAL}${RESET} ${BRIGHT_BLUE}./*${RESET}" + echo -e "${BRIGHT_CYAN}chmod${RESET} ${BRIGHT_CYAN}-R${RESET} ${BRIGHT_CYAN}u=${BRIGHT_GREEN}${EXAMPLE[1]}${RESET}${BRIGHT_CYAN},g=${BRIGHT_YELLOW}${EXAMPLE[2]}${RESET}${BRIGHT_CYAN},o=${BRIGHT_RED}${EXAMPLE[3]}${RESET} ${BRIGHT_BLUE}./*${RESET}" + + elif [[ "$#" -eq 3 ]]; then + local FORMATTED="" + local OCTAL_VALUE + for PERMISSION in "$@"; do + OCTAL_VALUE=0 + [[ ${PERMISSION} =~ .*r.* ]] && (( OCTAL_VALUE+=4 )) + [[ ${PERMISSION} =~ .*w.* ]] && (( OCTAL_VALUE+=2 )) + [[ ${PERMISSION} =~ .*x.* ]] && (( OCTAL_VALUE+=1 )) + FORMATTED="${FORMATTED}${OCTAL_VALUE}" + done + echo -e "${BRIGHT_CYAN}${FORMATTED}${RESET}" + chmodcalc "${FORMATTED}" + + else + echo -e "${BRIGHT_CYAN}chmodcalc${RESET}: Calculate and display chmod permissions" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}chmodcalc${RESET} ${BRIGHT_MAGENTA}<${BRIGHT_YELLOW}octal${BRIGHT_MAGENTA}>${RESET}" + echo -e " ${BRIGHT_CYAN}chmodcalc${RESET} ${BRIGHT_MAGENTA}<${BRIGHT_YELLOW}owner${BRIGHT_MAGENTA}>${RESET} ${BRIGHT_MAGENTA}<${BRIGHT_YELLOW}group${BRIGHT_MAGENTA}>${RESET} ${BRIGHT_MAGENTA}<${BRIGHT_YELLOW}other${BRIGHT_MAGENTA}>${RESET}" + echo -e "${BRIGHT_WHITE}Examples:${RESET}" + echo -e " ${BRIGHT_CYAN}chmodcalc${RESET} ${BRIGHT_YELLOW}755${RESET} ${BRIGHT_BLUE}# Show rwx for 755${RESET}" + echo -e " ${BRIGHT_CYAN}chmodcalc${RESET} ${BRIGHT_YELLOW}rwx rw r${RESET} ${BRIGHT_BLUE}# Convert to octal (754)${RESET}" + echo -e "${BRIGHT_WHITE}Octal values:${RESET} read=${BRIGHT_CYAN}4${RESET}, write=${BRIGHT_CYAN}2${RESET}, execute=${BRIGHT_CYAN}1${RESET}" + fi +} + +# Recursively set permissions for only files +function chmodfiles() { + # Initialize local variables + local _DIRECTORY="${2:-${PWD}}" # Default to current directory if no directory is provided + local _PERMISSION="$1" # The permission mode to be set + + # Check for missing permission parameter + if [[ -z "${_PERMISSION}" ]] || [[ "${1}" == "--help" ]] || [[ "${1}" == "-h" ]]; then + echo -e "${BRIGHT_CYAN}chmodfiles${RESET}: Set permissions for files recursively" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}chmodfiles${RESET} ${BRIGHT_MAGENTA}<${BRIGHT_YELLOW}mode${BRIGHT_MAGENTA}>${RESET} ${BRIGHT_MAGENTA}[${BRIGHT_GREEN}directory${BRIGHT_MAGENTA}]${RESET}" + echo -e "${BRIGHT_WHITE}Examples:${RESET}" + echo -e " ${BRIGHT_CYAN}chmodfiles${RESET} ${BRIGHT_YELLOW}664${RESET} ${BRIGHT_BLUE}# Current directory${RESET}" + echo -e " ${BRIGHT_CYAN}chmodfiles${RESET} ${BRIGHT_YELLOW}+x${RESET} ${BRIGHT_GREEN}/some/path${RESET} ${BRIGHT_BLUE}# Specific directory${RESET}" + return 1 + fi + + # Check if the specified directory exists + if [ ! -d "${_DIRECTORY}" ]; then + echo -e "${BRIGHT_RED}Error: ${BRIGHT_CYAN}Directory ${BRIGHT_YELLOW}${_DIRECTORY}${BRIGHT_CYAN} not found${RESET}" + return 2 + fi + + # Ask the user if sudo should be used for the operation + local SUDO_PREFIX='' + if ask "${BRIGHT_CYAN}Use administrator priveledges for this operation?${RESET}" N; then + SUDO_PREFIX='sudo ' + fi + + # Ask for confirmation + if ask "${BRIGHT_CYAN}Are you sure you want to change file permissions in ${BRIGHT_YELLOW}${_DIRECTORY}${BRIGHT_CYAN}?${RESET}" N; then + # Change permissions for files recursively + ${SUDO_PREFIX}find "${_DIRECTORY}" -type f -exec chmod "${_PERMISSION}" {} \; + echo -e "${BRIGHT_GREEN}Permissions set for files in ${BRIGHT_YELLOW}${_DIRECTORY}${RESET}" + else + echo -e "${BRIGHT_RED}Operation canceled${RESET}" + fi +} + +# Recursively set permissions for only directories +alias chmoddirectories='chmoddirs' +alias chmodfolders='chmoddirs' +function chmoddirs() { + # Initialize local variables + local _DIRECTORY="${2:-${PWD}}" # Default to current directory if no directory is provided + local _PERMISSION="$1" # The permission mode to be set + + # Check for missing permission parameter + if [[ -z "${_PERMISSION}" ]] || [[ "${1}" == "--help" ]] || [[ "${1}" == "-h" ]]; then + echo -e "${BRIGHT_CYAN}chmoddirs${RESET}: Set permissions for directories recursively" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}chmoddirs${RESET} ${BRIGHT_MAGENTA}<${BRIGHT_YELLOW}mode${BRIGHT_MAGENTA}>${RESET} ${BRIGHT_MAGENTA}[${BRIGHT_GREEN}directory${BRIGHT_MAGENTA}]${RESET}" + echo -e "${BRIGHT_WHITE}Examples:${RESET}" + echo -e " ${BRIGHT_CYAN}chmoddirs${RESET} ${BRIGHT_YELLOW}775${RESET} ${BRIGHT_BLUE}# Current directory${RESET}" + echo -e " ${BRIGHT_CYAN}chmoddirs${RESET} ${BRIGHT_YELLOW}+x${RESET} ${BRIGHT_GREEN}/some/path${RESET} ${BRIGHT_BLUE}# Specific directory${RESET}" + return 1 + fi + + # Check if the specified directory exists + if [ ! -d "${_DIRECTORY}" ]; then + echo -e "${BRIGHT_RED}Error: ${BRIGHT_CYAN}Directory ${BRIGHT_YELLOW}${_DIRECTORY}${BRIGHT_CYAN} not found${RESET}" + return 2 + fi + + # Ask the user if sudo should be used for the operation + local SUDO_PREFIX='' + if ask "${BRIGHT_CYAN}Use administrator priveledges for this operation?${RESET}" N; then + SUDO_PREFIX='sudo ' + fi + + # Confirm the action before proceeding + if ask "${BRIGHT_CYAN}Are you sure you want to change directory permissions in ${BRIGHT_YELLOW}${_DIRECTORY}${BRIGHT_CYAN}?${RESET}" N; then + # Change permissions for directories recursively + ${SUDO_PREFIX}find "${_DIRECTORY}" -type d -name \* -exec chmod "${_PERMISSION}" {} \; + echo -e "${BRIGHT_GREEN}Permissions set for directories in ${BRIGHT_YELLOW}${_DIRECTORY}${RESET}" + else + echo -e "${BRIGHT_RED}Operation canceled${RESET}" + fi +} + +# Recursively set permissions of code files and directories +# WARNING: Will add execute permissions to .sh files! +function chfix() { + # Check if -h or --help is provided as the first argument + if [[ "${1}" == "--help" ]] || [[ "${1}" == "-h" ]]; then + echo -e "${BRIGHT_CYAN}chfix${RESET}: Set permissions and ownership for files and directories recursively" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}chfix${RESET} ${BRIGHT_MAGENTA}[${BRIGHT_GREEN}file_mode${BRIGHT_MAGENTA}]${RESET} ${BRIGHT_MAGENTA}[${BRIGHT_GREEN}dir_mode${BRIGHT_MAGENTA}]${RESET} ${BRIGHT_MAGENTA}[${BRIGHT_GREEN}owner${BRIGHT_MAGENTA}]${RESET} ${BRIGHT_MAGENTA}[${BRIGHT_GREEN}directory${BRIGHT_MAGENTA}]${RESET}" + echo -e "${BRIGHT_WHITE}Defaults:${RESET} file_mode=664, dir_mode=775, directory=current" + echo -e "${BRIGHT_WHITE}Examples:${RESET}" + echo -e " ${BRIGHT_CYAN}chfix${RESET} ${BRIGHT_BLUE}# Use defaults${RESET}" + echo -e " ${BRIGHT_CYAN}chfix${RESET} ${BRIGHT_YELLOW}644 755${RESET} ${BRIGHT_BLUE}# Custom modes${RESET}" + echo -e " ${BRIGHT_CYAN}chfix${RESET} ${BRIGHT_YELLOW}644 755${RESET} ${BRIGHT_GREEN}www-data${RESET} ${BRIGHT_BLUE}# With owner${RESET}" + echo -e " ${BRIGHT_CYAN}chfix${RESET} ${BRIGHT_YELLOW}664 775${RESET} ${BRIGHT_GREEN}www-data:dev /var/www${RESET} ${BRIGHT_BLUE}# Full usage${RESET}" + return 1 + fi + + # Initialize variables with default values or passed arguments + # Write access is also given to the group (e.g. for a group of developers) + local FILE_PERMISSIONS="${1:-0664}" # Default to User:rw Group:rw Other:r + local DIR_PERMISSIONS="${2:-0775}" # Default to User:rwx Group:rwx Other:rx + local OWNER="${3:-}" # Optional owner + local DIRECTORY="${4:-${PWD}}" # Default to current directory + + # Check if the specified directory exists + if [ ! -d "${DIRECTORY}" ]; then + echo -e "${BRIGHT_RED}Error: ${BRIGHT_CYAN}Directory ${BRIGHT_YELLOW}${DIRECTORY}${BRIGHT_CYAN} not found${RESET}" + return 2 + fi + + # Ask the user if sudo should be used for the operation + local SUDO_PREFIX='' + if ask "${BRIGHT_CYAN}Use administrator privileges for this operation?${RESET}" N; then + SUDO_PREFIX='sudo ' + fi + + # Confirm the action + if ask "${BRIGHT_CYAN}This will change all permissions and optionally ownership for directories and files in ${BRIGHT_YELLOW}${DIRECTORY}${RESET}. Are you sure?${RESET}" N; then + # Change permissions + ${SUDO_PREFIX}find "${DIRECTORY}" -type f -exec chmod "${FILE_PERMISSIONS}" {} \; + ${SUDO_PREFIX}find "${DIRECTORY}" -type d -exec chmod "${DIR_PERMISSIONS}" {} \; + + # Add execute permissions to .sh files + ${SUDO_PREFIX}find "${DIRECTORY}" -type f -name "*.sh" -exec chmod +x {} \; + + # Change ownership if OWNER is specified + [ -n "${OWNER}" ] && ${SUDO_PREFIX}chown -R "${OWNER}" "${DIRECTORY}" + echo -e "${BRIGHT_GREEN}Permissions and ownership set for ${BRIGHT_YELLOW}${DIRECTORY}${RESET}" + fi +} + +# Copy/clone file permissions +function chmodcopy() { + # Check if at least two arguments are provided (source and destination) + if [ $# -lt 2 ]; then + # Display usage information if the required arguments are not provided + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}chmodcopy${RESET} ${BRIGHT_GREEN}[source_file]${RESET} ${BRIGHT_YELLOW}[destination_file]${RESET} ${BRIGHT_CYAN}[additional_optional_chmod_parameters]${RESET}" + return 1 + fi + + # Prompt the user to confirm if they want tu use administrator privileges + local SUDO_PREFIX='' + if ask "${BRIGHT_CYAN}Use administrator privileges for this operation?${RESET}" N; then + SUDO_PREFIX='sudo ' + fi + + # Execute the chmod command with --reference option + ${SUDO_PREFIX}chmod --reference="${@}" + echo -e "${BRIGHT_GREEN}Permissions copied${RESET}" +} + +# Improved terminal clipboard management for viewing, setting, and +# clearing content, with support for piping input and output. +function clipboard() { + # Help message + if [[ "$1" == "--help" || "$1" == "-h" ]]; then + echo -e "${BRIGHT_CYAN}clipboard${RESET}: Terminal clipboard management with piping support" + echo -e "${BRIGHT_WHITE}Usage:${RESET}" + echo -e " ${BRIGHT_CYAN}clipboard${RESET} ${BRIGHT_MAGENTA}<${BRIGHT_YELLOW}text${BRIGHT_MAGENTA}>${RESET} ${BRIGHT_BLUE}# Copy text to clipboard${RESET}" + echo -e " command | ${BRIGHT_CYAN}clipboard${RESET} ${BRIGHT_BLUE}# Copy command output to clipboard${RESET}" + echo -e " ${BRIGHT_CYAN}clipboard${RESET} | command ${BRIGHT_BLUE}# Paste clipboard to command${RESET}" + echo -e "${BRIGHT_WHITE}Examples:${RESET}" + echo -e " ${BRIGHT_CYAN}clipboard${RESET} ${BRIGHT_YELLOW}'Hello World'${RESET} ${BRIGHT_BLUE}# Copy text${RESET}" + echo -e " ${BRIGHT_YELLOW}cat file.txt${RESET} | ${BRIGHT_CYAN}clipboard${RESET} ${BRIGHT_BLUE}# Copy file contents${RESET}" + echo -e " ${BRIGHT_CYAN}clipboard${RESET} | ${BRIGHT_YELLOW}less${RESET} ${BRIGHT_BLUE}# View clipboard in less${RESET}" + echo -e " ${BRIGHT_YELLOW}pwd${RESET} | ${BRIGHT_CYAN}clipboard${RESET} ${BRIGHT_BLUE}# Copy current directory${RESET}" + return + fi + + # Local variables + local CLIPBOARD_CONTENT="" # Content for clipboard + local SEND_TO_CLIPBOARD=false # Flag if content needs to be sent to clipboard + + # If we are logged in via SSH... + if [[ -n "${SSH_CLIENT}" ]] || [[ -n "${SSH_TTY}" ]]; then + # No local clipboard access in SSH + return 1 + # Determine if text is passed as a parameter + elif [[ -n "${1}" ]]; then + CLIPBOARD_CONTENT="${1}" + SEND_TO_CLIPBOARD=true + # Determine if content is piped to clipboard + elif [[ -p /dev/stdin ]]; then + CLIPBOARD_CONTENT=$(cat -) + SEND_TO_CLIPBOARD=true + fi + + # Handle content that needs to be sent to clipboard + if ${SEND_TO_CLIPBOARD}; then + if [[ "${OSTYPE}" == "darwin"* ]] && hascommand --strict pbcopy; then + # macOS + echo -n "${CLIPBOARD_CONTENT}" | pbcopy + elif hascommand --strict wl-copy && [[ "${XDG_SESSION_TYPE}" == "wayland" ]]; then + # Wayland + echo -n "${CLIPBOARD_CONTENT}" | wl-copy &>/dev/null + elif hascommand --strict xclip; then + # X11 - xclip + echo -n "${CLIPBOARD_CONTENT}" | xclip -selection clipboard &>/dev/null + elif hascommand --strict xsel; then + # X11 - xsel + echo -n "${CLIPBOARD_CONTENT}" | xsel -ib &>/dev/null + fi + else + # Handle content that needs to be pasted from clipboard + if [[ "${OSTYPE}" == "darwin"* ]] && hascommand --strict pbpaste; then + # macOS + pbpaste + elif hascommand --strict wl-paste && [[ "${XDG_SESSION_TYPE}" == "wayland" ]]; then + # Wayland + wl-paste + elif hascommand --strict xclip; then + # X11 - xclip + xclip -o -selection clipboard + elif hascommand --strict xsel; then + # X11 - xsel + xsel -ob + fi + fi + + # If Tmux is running and there is content for the clipboard... + if [[ -n "${TMUX}" ]] && [[ -n "${CLIPBOARD_CONTENT}" ]]; then + echo -n "${CLIPBOARD_CONTENT}" | tmux loadb - + fi +} + +# Copy a file's contents to the clipboard +function file2cb() { + # Check for parameters... + if [[ $# -eq 0 ]]; then + echo -e "${BRIGHT_WHITE}Usage: ${BRIGHT_CYAN}file2cb ${BRIGHT_YELLOW}[filename]${BRIGHT_WHITE}" + echo -e "${BRIGHT_WHITE}Copy a file's contents to the clipboard.${RESET}" + return 1 + # Check if the file exists... + elif [[ ! -f "$1" ]]; then + echo -e "${BRIGHT_RED}Error: ${BRIGHT_CYAN}The file ${BRIGHT_YELLOW}$1${BRIGHT_CYAN} does not exist.${RESET}" + return 1 + fi + command cat "$1" | clipboard +} + +# Save the clipboard contents to a file +function cb2file() { + if [[ $# -eq 0 ]]; then + echo -e "${BRIGHT_WHITE}cb2file:${RESET} Save the clipboard contents to a file" + echo -e "${BRIGHT_WHITE}Usage:${BRIGHT_CYAN} cb2file${RESET} ${BRIGHT_YELLOW}${RESET}" + return 1 + fi + clipboard > "$1" +} + +# Dump the clipboard contents to the console +function cbshow() { + # Help text + if [[ "${1}" == "--help" ]] || [[ "${1}" == "-h" ]]; then + echo -e "${BRIGHT_CYAN}cbshow${RESET}: Display current clipboard contents" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}cbshow${RESET}" + return 0 + fi + + clipboard +} + +# When using the which command, copy the output to the clipboard or Tmux buffer +alias which='_which_to_clipboard' +# Call the 'which' command and copy its output to clipboard +function _which_to_clipboard() { + # Get the path of the requested which command + local WHICH_OUTPUT=$(command which "$@" 2>&1) + + # Capture the exit status of the 'which' command + local EXIT_STATUS=$? + + # Check if 'which' output is non-empty and not "/usr/bin/grc" + if [[ ${EXIT_STATUS} -eq 0 ]] && [[ "${WHICH_OUTPUT}" != "/usr/bin/grc" ]]; then + # Copy the output to clipboard and display in terminal + echo "${WHICH_OUTPUT}" | clipboard + fi + + # Display the standard output of 'which' + echo "${WHICH_OUTPUT}" + + # Return the captured exit status + return ${EXIT_STATUS} +} + +# When using the pwd command, copy the directory to the clipboard or Tmux buffer +alias pwd='_pwd_to_clipboard' +function _pwd_to_clipboard() { + # Run the real pwd command and capture its output + local _PWD_OUTPUT="$(command pwd "$@")" + echo "${_PWD_OUTPUT}" + + # If we are not in the enhancd program folder... + if [[ "${PWD}" != *"/enhancd" ]]; then + # Use the new clipboard command to copy the output + clipboard "${_PWD_OUTPUT}" + fi +} + +# Compress a file (even binary files) to the clipboard as base64 text +function file2asc() { + # Check if a parameter was passed and if the file exists + if [[ ${#} -eq 0 ]] || [[ ! -f "${1}" ]]; then + echo -e "${BRIGHT_WHITE}Usage: ${BRIGHT_CYAN}file2asc${RESET} ${BRIGHT_GREEN}[filename]${RESET}" + return 1 + fi + + # Check if gzip is installed + if ! hascommand --strict gzip; then + >&2 echo "Requires gzip/gunzip" + return 1 + fi + + # Create the base64-encoded gzipped content + # SC2155: split declaration so gzip/base64 failure isn't masked + local ENCODED_CONTENT + ENCODED_CONTENT=$(command cat "${1}" | gzip -9 | base64) + + # Send the content to clipboard + if clipboard "${ENCODED_CONTENT}"; then + echo "The clipboard now contains the file: ${1}" + else + # If clipboard function couldn't handle the content, fallback + + # We might have a headless environment with no clipboard + if [[ -z "${PAGER}" ]]; then + printf '=%.0s' {1..80}; echo + echo "${ENCODED_CONTENT}" + printf '=%.0s' {1..80}; echo + else # Use the pager + # To save text as a file from less, type s then type the file name + echo "${ENCODED_CONTENT}" | "${PAGER}" + fi + fi + + # Done! + echo "Use asc2file to convert the base64 ASCII text to a file." +} + +# Convert compressed base64 clipboard back to a file +function asc2file() { + # If no parameters... + if [[ ${#} -eq 0 ]]; then + echo -e "${BRIGHT_WHITE}Usage: ${BRIGHT_CYAN}asc2file${RESET} ${BRIGHT_GREEN}[filename]${RESET}" + return 1 + fi + + # Check for gunzip + if ! hascommand --strict gunzip; then + >&2 echo "Requires gzip/gunzip" + return 1 + fi + + # Fetch the current clipboard content + local CLIPBOARD_CONTENT=$(clipboard) + if [[ -z "${CLIPBOARD_CONTENT}" ]]; then + echo -e "${BRIGHT_YELLOW}Paste the text and press ${BRIGHT_CYAN}CTRL+d${BRIGHT_YELLOW} when done:${RESET} " + CLIPBOARD_CONTENT=$( "${1}" + + # Display the saved file path + if hascommand --strict realpath; then + echo "The file was saved: $(command realpath "${1}")" + else + echo "The file was saved: ${1}" + fi +} + +# Convert an image to compressed JPG format +alias {png2jpg,image2jpg}='compressimage' +function compressimage() { + # Show help message if no parameters are provided + if [[ -z "$1" ]] || [[ "${1}" == "--help" ]] || [[ "${1}" == "-h" ]]; then + echo -e "${BRIGHT_CYAN}compressimage${RESET}: Convert an image to compressed JPG format" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}compressimage${RESET} ${BRIGHT_MAGENTA}<${BRIGHT_YELLOW}image${BRIGHT_MAGENTA}>${RESET} ${BRIGHT_MAGENTA}[${BRIGHT_GREEN}quality${BRIGHT_MAGENTA}]${RESET}" + echo -e "${BRIGHT_WHITE}Defaults:${RESET} quality=85 (range 1-100)" + echo -e "${BRIGHT_WHITE}Examples:${RESET}" + echo -e " ${BRIGHT_CYAN}compressimage${RESET} ${BRIGHT_YELLOW}screenshot.png${RESET} ${BRIGHT_BLUE}# Use default quality${RESET}" + echo -e " ${BRIGHT_CYAN}compressimage${RESET} ${BRIGHT_YELLOW}photo.png${RESET} ${BRIGHT_GREEN}90${RESET} ${BRIGHT_BLUE}# Higher quality${RESET}" + echo -e " ${BRIGHT_CYAN}compressimage${RESET} ${BRIGHT_YELLOW}large.png${RESET} ${BRIGHT_GREEN}60${RESET} ${BRIGHT_BLUE}# More compression${RESET}" + return 1 + fi + + # Check if ImageMagick's 'convert' and 'identify' commands are available + if ! hascommand --strict convert || ! hascommand --strict identify; then + echo -e "${BRIGHT_RED}Error: ${BRIGHT_CYAN}ImageMagick is not installed. Please install it first.${RESET}" + return 1 + fi + + # Check if the input image file exists + if [ ! -f "$1" ]; then + echo -e "${BRIGHT_RED}Error: ${BRIGHT_CYAN}The input image file ${BRIGHT_YELLOW}$1${BRIGHT_CYAN} does not exist.${RESET}" + return 1 + fi + + # Default quality setting + local QUALITY=85 + + # Validate optional quality argument + if [ ! -z "$2" ]; then + if [[ ! "$2" =~ ^[0-9]+$ ]] || [ "$2" -le 0 ] || [ "$2" -gt 100 ]; then + echo -e "${BRIGHT_RED}Error: ${BRIGHT_CYAN}Invalid quality percentage. It should be a positive integer between 1 and 100.${RESET}" + return 1 + fi + QUALITY=$2 + fi + + # Extract file name and output to the same directory as the source file + local INPUT_FILENAME + INPUT_FILENAME=$(basename -- "$1") + local INPUT_DIR + INPUT_DIR=$(dirname -- "$1") + local OUTPUT_FILENAME="${INPUT_DIR}/${INPUT_FILENAME%.*}.jpg" + + # Perform the conversion + convert "$1" -quality ${QUALITY} "${OUTPUT_FILENAME}" + + echo -e "${BRIGHT_GREEN}Success: ${BRIGHT_WHITE}Converted ${BRIGHT_YELLOW}$1${BRIGHT_WHITE} to ${BRIGHT_YELLOW}${OUTPUT_FILENAME}${BRIGHT_WHITE} with quality ${BRIGHT_YELLOW}${QUALITY}%${RESET}." +} + +# Converts a markdown title string into a markdown tag +alias md2tag='convert2mdtag' +function convert2mdtag() { + # Help text + if [[ -z "${1}" ]] || [[ "${1}" == "--help" ]] || [[ "${1}" == "-h" ]]; then + echo -e "${BRIGHT_CYAN}convert2mdtag${RESET}: Convert a heading to a markdown anchor link" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}convert2mdtag${RESET} ${BRIGHT_MAGENTA}<${BRIGHT_YELLOW}heading${BRIGHT_MAGENTA}>${RESET}" + echo -e "${BRIGHT_WHITE}Description:${RESET}" + echo -e " ${BRIGHT_BLUE}Creates [Title](#anchor) format, copies to clipboard${RESET}" + echo -e "${BRIGHT_WHITE}Examples:${RESET}" + echo -e " ${BRIGHT_CYAN}convert2mdtag${RESET} ${BRIGHT_YELLOW}'## My Section'${RESET} ${BRIGHT_BLUE}# [My Section](#my-section)${RESET}" + echo -e " ${BRIGHT_CYAN}convert2mdtag${RESET} ${BRIGHT_YELLOW}'Hello World'${RESET} ${BRIGHT_BLUE}# [Hello World](#hello-world)${RESET}" + return 0 + fi + + # Trim leading '#' or spaces and trailing spaces + local TRIMMED_INPUT=$(echo "${1}" | sed -e 's/^[\# ]*//' -e 's/[ ]*$//') + + # Convert to lowercase, replace spaces with hyphens, + # remove unwanted characters, and fix double dashes + local TAG=$(echo "${TRIMMED_INPUT,,}" | sed -e 's/ /-/g' | tr -dc 'a-z0-9-' | sed 's/--*/-/g') + + # Output the result in the desired markdown link format + echo "[${TRIMMED_INPUT}](#${TAG})" | clipboard + echo "[${TRIMMED_INPUT}](#${TAG})" +} + +# Remove unneeded execute permissions from strictly non-executable file types +# It is good practice to remove execute permissions from data files to enhance +# security, as files masquerading as these types could potentially be malicious +function fixinvalidexecutepermissions() { + # Parse arguments to check for help flag + local TARGET_DIRECTORY + local SHOW_HELP=false + + # Process parameters + while [[ $# -gt 0 ]]; do + case "${1}" in + -h|--help) + SHOW_HELP=true + shift + ;; + *) + # First non-flag argument is treated as target directory + if [[ -z "${TARGET_DIRECTORY}" ]]; then + TARGET_DIRECTORY="${1}" + fi + shift + ;; + esac + done + + # Show help if requested + if ${SHOW_HELP}; then + echo -e "${BRIGHT_CYAN}fixinvalidexecutepermissions${RESET}: Remove execute permissions from non-executable file types" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}fixinvalidexecutepermissions${RESET} ${BRIGHT_MAGENTA}[${BRIGHT_GREEN}directory${BRIGHT_MAGENTA}]${RESET}" + echo -e "${BRIGHT_WHITE}Description:${RESET}" + echo -e " ${BRIGHT_BLUE}Removes +x from docs, images, fonts, archives, etc.${RESET}" + echo -e " ${BRIGHT_BLUE}Enhances security by preventing accidental execution${RESET}" + echo -e "${BRIGHT_WHITE}Examples:${RESET}" + echo -e " ${BRIGHT_CYAN}fixinvalidexecutepermissions${RESET} ${BRIGHT_BLUE}# Current directory${RESET}" + echo -e " ${BRIGHT_CYAN}fixinvalidexecutepermissions${RESET} ${BRIGHT_GREEN}~/Documents${RESET} ${BRIGHT_BLUE}# Specific directory${RESET}" + return 0 + fi + + # Use current directory as default if no directory specified + TARGET_DIRECTORY="${TARGET_DIRECTORY:-.}" + + # Check if the user can execute sudo commands + if ! sudo -v; then + echo -e "${BRIGHT_RED}Error:${RESET} You do not have sufficient permissions to run this script with necessary privileges" + return 1 + fi + + # Confirmation prompt using the provided ask function + if ask "${BRIGHT_MAGENTA}Do you want to remove invalid execute permissions in directory: ${BRIGHT_CYAN}${TARGET_DIRECTORY}${RESET}" Y; then + # Ask about including Git directories + local INCLUDE_GIT=false + if ask "${BRIGHT_MAGENTA}Include Git directories for execute permissions cleanup?${RESET} ${BRIGHT_YELLOW}(Not recommended)${RESET}" N; then + INCLUDE_GIT=true + fi + + # Document and Text Files + local -a doc_extensions=( + "ans" "asc" "azw" "azw3" "azw4" "cfg" "chm" "clf" "cnf" "conf" "csv" + "doc" "docx" "dot" "dotx" "ebook" "eml" "epub" "fdf" "hlp" "htm" + "html" "htmlz" "ics" "inf" "ini" "lng" "log" "lst" "mcw" "mht" + "mobi" "mpp" "msg" "nfo" "odp" "ods" "odt" "opf" "org" "ott" "pdf" + "plist" "po" "pot" "pps" "ppt" "pptx" "prn" "properties" "rpt" "rst" + "rtf" "slk" "sxw" "template" "tex" "toc" "txt" "url" "wb1" "wk1" + "wk3" "wk4" "wp5" "wpd" "wps" "wri" "wtf" "xls" "xlsm" "xlsx" + "xlsxm" "xlt" "xlw" "xps" "xslt" + ) + + # Non-Executable Code and Markup Files (should never contain a shebang) + local -a code_extensions=( + "ascx" "c" "cc" "config" "cpp" "css" "ctl" "cxx" "dtd" "editorconfig" + "exp" "frx" "h" "handlebars" "hpp" "htc" "inc" "jshintrc" "json" + "jsonl" "kwinrule" "less" "mak" "manifest" "md" "mm" "mo" "proj" + "project" "pxd" "qml" "rc" "res" "resx" "sass" "scc" "scss" "sql" + "strings" "tpl" "vsixmanifest" "xhtml" "xlf" "xml" "xsd" "yaml" "yml" + ) + + # Font Files + local -a font_extensions=( + "afm" "eot" "fnt" "fon" "otf" "ttc" "ttf" "ufm" "woff" "woff2" + ) + + # Image Files + local -a image_extensions=( + "afdesign" "afphoto" "ai" "ani" "blp" "bmp" "bpg" "bw" "cbz" "cbr" + "cdr" "cgm" "cpt" "cur" "dcx" "dib" "drw" "emf" "eps" "exif" "gif" + "hdr" "heic" "heif" "ico" "idml" "iff" "ilbm" "img" "indd" "indl" + "indt" "jfif" "jif" "jpeg" "jpg" "kdc" "lbm" "pbm" "pcd" "pcx" "pdn" + "pgm" "pic" "pix" "png" "pnm" "ppm" "ps" "psd" "psp" "pspimage" + "raw" "rgb" "rgba" "rle" "sgi" "spp" "svg" "svgz" "tga" "tif" "tiff" + "vml" "webp" "wmf" "xcf" "xisf" "xmp" + ) + + # Audio Files + local -a audio_extensions=( + "8svx" "aa" "aac" "aax" "acm" "act" "aip" "amr" "as" "au" "awb" + "cda" "dct" "dsm" "dss" "dvf" "dwd" "flac" "gsm" "iklax" "it" "ivs" + "m3u" "m4a" "m4b" "m4p" "mid" "midi" "mmf" "mod" "mogg" "mp1" "mp2" + "mp3" "mpa" "mpc" "msv" "mtm" "nmf" "nsf" "oga" "ogg" "opus" "pcm" + "pls" "ra" "rmi" "s3m" "sam" "sln" "smp" "snd" "stm" "svx" "vba" + "vce" "voc" "vox" "wav" "wma" "wv" "xm" + ) + + # Video Files + local -a video_extensions=( + "3g2" "3gp" "asf" "asx" "avi" "divx" "fla" "flv" "m1v" "m4v" "mkv" + "mov" "mp4" "mpe" "mpeg" "mpg" "mpv" "qt" "qtif" "ram" "rm" "rmd" + "rmm" "rp" "rt" "smi" "smil" "swf" "ts" "viv" "vob" "webm" "wmv" + ) + + # Subtitle Files + local -a subtitle_extensions=( + "srt" "sub" "vtt" + ) + + # Gaming and Emulation Files + local -a gaming_emulation_extensions=( + "chd" "gba" "gcm" "nds" "nes" "rom" "sav" "sfc" "state" "wad" + ) + + # Archive Files + local -a archive_extensions=( + "7z" "ace" "arj" "bz2" "cab" "csv.gz" "cue" "deb" "dmg" "ex_" "gz" + "iso" "isz" "lha" "log.gz" "lzh" "mdf" "mds" "nrg" "pak" "pkg" "rar" + "rar5" "rpm" "sitx" "tar" "tar.bz2" "tar.gz" "tgz" "xpi" "z" "zip" + "zipx" + ) + + # Backup and Temporary Files + local -a backup_temp_extensions=( + "~" "bac" "backup" "bak" "bkp" "old" "orig" "original" "snapshot" + "swp" "temp" "tmp" + ) + + # Checksum and Security Files + local -a checksum_security_extensions=( + "crc" "md5" "md5sum" "par" "par2" "sha1" "sha1sum" "sha256" + "sha256sum" "sha512" + ) + + # Key and Certificate Files + local -a key_certs_extensions=( + "cer" "cert" "crt" "csr" "der" "jks" "key" "p12" "pfx" "pem" "ppk" + "pub" "pubkey" "sig" + ) + + # Database Files + local -a database_extensions=( + "accdb" "db" "db3" "dbf" "idx" "mdb" "sqlite" "sqlite3" + ) + + # Miscellaneous Utilities + local -a misc_utilities_extensions=( + "cat" "chk" "cpl" "dat" "dll" "dmp" "drv" "dylib" "icl" "lib" "ocb" + "ocx" "opml" "ost" "part" "pdb" "prefs" "pst" "reg" "rnd" "rng" + "sys" "tlb" "torrent" "vxd" "wasm" + ) + + # Configuration Files + local -a config_files=( + ".aspell.*.prepl" ".aspell.*.pws" ".bash_history" ".bash_logout" + ".bash_profile" ".bashrc" ".cshrc" ".csslintrc" ".cvsignore" + ".dircolors" ".dmrc" ".eslintrc" ".env" ".gitconfig" ".gitignore" + ".gitmodules" ".gtkrc-2.0" ".htaccess" ".htpasswd" ".ICEauthority" + ".inputrc" "Jenkinsfile" ".kshrc" ".lesshst" ".nanorc" + ".node_repl_history" ".npmignore" ".nvmrc" ".rnd" ".screenrc" + ".steampath" ".steampid" ".tmux.conf" ".vimrc" ".vuescanrc" + ".wget-hsts" ".Xauthority" ".Xclients" ".xinitrc" ".Xmodmap" + ".xprofile" ".Xresources" ".xscreensaver" ".yarnrc" ".zshrc" + ) + + # Combine all extensions into one array + local -a all_extensions=( + "${doc_extensions[@]}" + "${code_extensions[@]}" + "${font_extensions[@]}" + "${image_extensions[@]}" + "${audio_extensions[@]}" + "${video_extensions[@]}" + "${subtitle_extensions[@]}" + "${gaming_emulation_extensions[@]}" + "${archive_extensions[@]}" + "${backup_temp_extensions[@]}" + "${checksum_security_extensions[@]}" + "${key_certs_extensions[@]}" + "${database_extensions[@]}" + "${misc_utilities_extensions[@]}" + ) + + # Build the find command arguments for file extensions and config files + local -a FIND_ARGS=() + local EXTENSION + for EXTENSION in "${all_extensions[@]}"; do + FIND_ARGS+=( -iname "*.${EXTENSION}" -o ) + done + + # Add patterns for configuration files (exact filenames) + local FILENAME + for FILENAME in "${config_files[@]}"; do + FIND_ARGS+=( -iname "${FILENAME}" -o ) + done + + # Remove the last '-o' (logical OR) to prevent syntax error + unset 'FIND_ARGS[${#FIND_ARGS[@]}-1]' + + # Construct find command based on whether to include Git directories + if ${INCLUDE_GIT}; then + # Include Git directories in the search + sudo find "${TARGET_DIRECTORY}" \ + -type f \( "${FIND_ARGS[@]}" \) \ + -exec chmod a-x {} + + echo -e "${BRIGHT_GREEN}✓${RESET} Execute permissions have been removed from non-executable files ${BRIGHT_YELLOW}(including Git directories)${RESET}" + else + # Exclude Git directories from the search using prune + sudo find "${TARGET_DIRECTORY}" \ + -type d -name ".git" -prune -o \ + -type f \( "${FIND_ARGS[@]}" \) \ + -exec chmod a-x {} + + echo -e "${BRIGHT_GREEN}✓${RESET} Execute permissions have been removed from non-executable files ${BRIGHT_BLUE}(Git directories excluded)${RESET}" + fi + else + return 1 + fi +} + +# Fixes permissions in a user's home folder +# If no user is specified, the current user is selected +function fixuserhome() { + # Validate input parameters + local _username + if [[ -z "${1}" ]]; then + _username="$(whoami)" + else + _username="${1}" + fi + + # Attempt to retrieve the home directory + # SC2155: split declaration so getent failure isn't masked + local _home_dir + _home_dir=$(getent passwd "${_username}" 2>/dev/null | cut -d: -f6) + + # Check if the retrieved directory is valid and exists + if [[ -z "${_home_dir}" ]] || [[ ! -d "${_home_dir}" ]]; then + # Default to /home/username if no valid directory is found + _home_dir="/home/${_username}" + fi + + # Verify if the home folder exists + if [ -d "${_home_dir}" ]; then + echo -e "Home directory found: ${BRIGHT_YELLOW}${_home_dir}${RESET}" + else + echo -e "${BRIGHT_RED}Error:${RESET} User ${BRIGHT_CYAN}${_username}${RESET} does not have a home directory" + return 1 + fi + + # Check if the user can execute sudo commands + if ! sudo -v; then + echo -e "${BRIGHT_RED}Error:${RESET} You do not have sufficient permissions to run this script with necessary privileges" + return 1 + fi + + # Confirm with the user + if ask "${BRIGHT_RED}WARNING:${RESET} Change all permissions for user ${BRIGHT_CYAN}${_username}${RESET}'s home folder?" N; then + + if ask "Reset group ownership permissions to ${BRIGHT_CYAN}${_username}${RESET}?" Y; then + runwithfeedback \ + "Set the owner and group as ${_username}" \ + "sudo chown -R '${_username}':'${_username}' '${_home_dir}'" + else + runwithfeedback \ + "Set the owner and group as ${_username}" \ + "sudo chown -R '${_username}' '${_home_dir}'" + fi + + # Remove unneeded execute permissions from strictly non-executable file types + fixinvalidexecutepermissions "${_home_dir}" + + runwithfeedback \ + "Make sure we have read and write access" \ + "chmod -R u+rw '${_home_dir}'" + + runwithfeedback \ + "Remove write access from group" \ + "chmod -R g-w '${_home_dir}'" + + runwithfeedback \ + "Remove all access from others" \ + "chmod -R o-rwx '${_home_dir}'" + + runwithfeedback \ + "Make .sh shell script files executable" \ + "find '${_home_dir}' -type f \( -name \"*.sh\" -o -name \".*.sh\" \) -exec chmod ug+x {} \;" + + runwithfeedback \ + "Make sure all directories have execute permissions" \ + "chmod -R ug+X '${_home_dir}'" + + # Remove group permissions for directories without group read + runwithfeedback \ + "Remove group permissions for directories without group read" \ + "find '${_home_dir}' -type d ! -perm -g+r -execdir chmod g-wx {} \;" + + if [[ -d "${_home_dir}/.local/share/kwalletd" ]]; then + runwithfeedback \ + "User only access to KDE Wallet keyring" \ + "chmod -R go-rwx '${_home_dir}/.local/share/kwalletd'" + fi + + # If there is a ~/.local/share/keyrings directory... + if [[ -d "${_home_dir}/.local/share/keyrings" ]]; then + runwithfeedback \ + "User only access to GNOME keyring" \ + "chmod -R go-rwx '${_home_dir}/.local/share/keyrings'" + fi + + # If there is an .ssh directory... + if [[ -d "${_home_dir}/.ssh" ]]; then + # Setting ownership for .ssh directory and files + runwithfeedback \ + "Setting ownership for .ssh directory and files" \ + "chown -R '${_username}':'${_username}' '${_home_dir}/.ssh'" + + # Setting strict permissions for .ssh and private keys + runwithfeedback \ + "User only access to .ssh and private keys" \ + "chmod -R go-rwx '${_home_dir}/.ssh'" + fi + + # If there is a .putty directory... + if [[ -d "${_home_dir}/.putty" ]]; then + runwithfeedback \ + "User only access to .putty and ssh keys" \ + "chmod -R go-rwx '${_home_dir}/.putty'" + fi + + # If there is a .pki directory... + if [[ -d "${_home_dir}/.pki" ]]; then + runwithfeedback \ + "User only access to .pki keys and certificates" \ + "chmod -R go-rwx '${_home_dir}/.pki'" + fi + + # If there is a .gnupg directory... + if [[ -d "${_home_dir}/.gnupg" ]]; then + runwithfeedback \ + "User only access to .gnupg and private keys" \ + "chmod -R go-rwx '${_home_dir}/.gnupg'" + fi + + # If KeePassXC/KeePass/KeeWeb is installed... + if hascommand --strict keepassxc || hascommand --strict keepass || hascommand --strict keeweb; then + runwithfeedback \ + "User only access to KeePassXC/KeePass/KeeWeb .kdbx files" \ + "find '${_home_dir}' -type f \( -name '*.kdbx' -o -name '.*.kdbx' \) -exec chmod go-rwx {} \;" + fi + + # If there is a pass directory... + if [[ -d "${_home_dir}/.password-store" ]]; then + runwithfeedback \ + "User only access to pass data" \ + "chmod -R go-rwx '${_home_dir}/.password-store'" + fi + + # If there is a Bitwarden directory... + if [[ -d "${_home_dir}/.config/Bitwarden" ]]; then + runwithfeedback \ + "User only access to Bitwarden data" \ + "chmod -R go-rwx '${_home_dir}/.config/Bitwarden'" + fi + if [[ -d "${_home_dir}/.var/app/com.bitwarden.desktop" ]]; then + runwithfeedback \ + "User only access to Bitwarden data (Flatpak)" \ + "chmod -R go-rwx '${_home_dir}/.var/app/com.bitwarden.desktop'" + fi + + # Check for Tor settings + if [[ -d "${_home_dir}/.local/share/torbrowser" ]]; then + runwithfeedback \ + "User only access to Tor browser data" \ + "chmod -R go-rwx '${_home_dir}/.local/share/torbrowser'" + fi + if [[ -d "${_home_dir}/.var/app/com.github.micahflee.torbrowser-launcher" ]]; then + runwithfeedback \ + "User only access to Tor browser data (Flatpak)" \ + "chmod -R go-rwx '${_home_dir}/.var/app/com.github.micahflee.torbrowser-launcher'" + fi + + # Check for Brave settings + if [[ -d "${_home_dir}/.config/BraveSoftware" ]]; then + runwithfeedback \ + "User only access to Brave browser data" \ + "chmod -R go-rwx '${_home_dir}/.config/BraveSoftware'" + fi + if [[ -d "${_home_dir}/.var/app/com.brave.Browser" ]]; then + runwithfeedback \ + "User only access to Brave browser data (Flatpak)" \ + "chmod -R go-rwx '${_home_dir}/.var/app/com.brave.Browser'" + fi + + # Check for Chrome settings + if [[ -d "${_home_dir}/.config/google-chrome" ]]; then + runwithfeedback \ + "User only access to Chrome browser data" \ + "chmod -R go-rwx '${_home_dir}/.config/google-chrome'" + fi + if [[ -d "${_home_dir}/.var/app/com.google.Chrome" ]]; then + runwithfeedback \ + "User only access to Chrome browser data (Flatpak)" \ + "chmod -R go-rwx '${_home_dir}/.var/app/com.google.Chrome'" + fi + + # Check for Chromium settings + if [[ -d "${_home_dir}/.config/chromium" ]]; then + runwithfeedback \ + "User only access to Chromium browser data" \ + "chmod -R go-rwx '${_home_dir}/.config/chromium'" + fi + if [[ -d "${_home_dir}/.var/app/org.chromium.Chromium" ]]; then + runwithfeedback \ + "User only access to Chromium browser data (Flatpak)" \ + "chmod -R go-rwx '${_home_dir}/.var/app/org.chromium.Chromium'" + fi + if [[ -d "${_home_dir}/.var/app/net.sourceforge.chromium-bsu" ]]; then + runwithfeedback \ + "User only access to Ungoogled Chromium browser data (Flatpak)" \ + "chmod -R go-rwx '${_home_dir}/.var/app/net.sourceforge.chromium-bsu'" + fi + + # Check for Firefox settings + if [[ -d "${_home_dir}/.mozilla" ]]; then + runwithfeedback \ + "User only access to Firefox browser data" \ + "chmod -R go-rwx '${_home_dir}/.mozilla'" + fi + if [[ -d "${_home_dir}/.var/app/org.mozilla.firefox" ]]; then + runwithfeedback \ + "User only access to Firefox browser data (Flatpak)" \ + "chmod -R go-rwx '${_home_dir}/.var/app/org.mozilla.firefox'" + fi + + # Check for LibreWolf settings + if [[ -d "${_home_dir}/.librewolf" ]]; then + runwithfeedback \ + "User only access to LibreWolf browser data" \ + "chmod -R go-rwx '${_home_dir}/.librewolf'" + fi + if [[ -d "${_home_dir}/.var/app/io.gitlab.librewolf-community" ]]; then + runwithfeedback \ + "User only access to LibreWolf browser data (Flatpak)" \ + "chmod -R go-rwx '${_home_dir}/.var/app/io.gitlab.librewolf-community'" + fi + + # Check for Opera settings + if [[ -d "${_home_dir}/.config/opera" ]]; then + runwithfeedback \ + "User only access to Opera browser data" \ + "chmod -R go-rwx '${_home_dir}/.config/opera'" + fi + + # Check for Vivaldi settings + if [[ -d "${_home_dir}/.config/vivaldi" ]]; then + runwithfeedback \ + "User only access to Vivaldi browser data" \ + "chmod -R go-rwx '${_home_dir}/.config/vivaldi'" + fi + + # Check for Microsoft Edge settings + if [[ -d "${_home_dir}/.config/microsoft-edge" ]]; then + runwithfeedback \ + "User only access to Microsoft Edge browser data" \ + "chmod -R go-rwx '${_home_dir}/.config/microsoft-edge'" + fi + if [[ -d "${_home_dir}/.config/microsoft-edge-beta" ]]; then + runwithfeedback \ + "User only access to Microsoft Edge browser data" \ + "chmod -R go-rwx '${_home_dir}/.config/microsoft-edge-beta'" + fi + + # Check for Evolution settings + if [[ -d "${_home_dir}/.config/evolution" ]]; then + runwithfeedback \ + "User only access to Evolution email data" \ + "chmod -R go-rwx '${_home_dir}/.config/evolution'" + fi + if [[ -d "${_home_dir}/.var/app/org.gnome.Evolution" ]]; then + runwithfeedback \ + "User only access to Evolution email data (Flatpak)" \ + "chmod -R go-rwx '${_home_dir}/.var/app/org.gnome.Evolution'" + fi + + # Check for Geary settings + if [[ -d "${_home_dir}/.local/share/geary" ]]; then + runwithfeedback \ + "User only access to Geary email data" \ + "chmod -R go-rwx '${_home_dir}/.local/share/geary'" + fi + if [[ -d "${_home_dir}/.var/app/org.gnome.Geary" ]]; then + runwithfeedback \ + "User only access to Geary email data (Flatpak)" \ + "chmod -R go-rwx '${_home_dir}/.var/app/org.gnome.Geary'" + fi + + # Check for Thunderbird settings + if [[ -d "${_home_dir}/.thunderbird" ]]; then + runwithfeedback \ + "User only access to Thunderbird email data" \ + "chmod -R go-rwx '${_home_dir}/.thunderbird'" + fi + if [[ -d "${_home_dir}/.var/app/org.mozilla.Thunderbird" ]]; then + runwithfeedback \ + "User only access to Thunderbird email data (Flatpak)" \ + "chmod -R go-rwx '${_home_dir}/.var/app/org.mozilla.Thunderbird'" + fi + + # Check for Element settings + if [[ -d "${_home_dir}/.config/Element" ]]; then + runwithfeedback \ + "User only access to Element chat data" \ + "chmod -R go-rwx '${_home_dir}/.config/Element'" + fi + if [[ -d "${_home_dir}/.var/app/im.riot.Riot" ]]; then + runwithfeedback \ + "User only access to Element chat data (Flatpak)" \ + "chmod -R go-rwx '${_home_dir}/.var/app/im.riot.Riot'" + fi + + # Check for Signal settings + if [[ -d "${_home_dir}/.config/Signal" ]]; then + runwithfeedback \ + "User only access to Signal chat data" \ + "chmod -R go-rwx '${_home_dir}/.config/Signal'" + fi + if [[ -d "${_home_dir}/.var/app/org.signal.Signal" ]]; then + runwithfeedback \ + "User only access to Signal chat data (Flatpak)" \ + "chmod -R go-rwx '${_home_dir}/.var/app/org.signal.Signal'" + fi + + if [[ -f "${_home_dir}/.config/birthdays.csv" ]]; then + runwithfeedback \ + "User only access to birthday/anniversary reminder data" \ + "chmod 600 '${_home_dir}/.config/birthdays.csv'" + elif [[ -f "${_BDAY_FILE}" ]]; then + runwithfeedback \ + "User only access to birthday/anniversary reminder data (from variable)" \ + "chmod 600 \"${_BDAY_FILE}\"" + fi + + # [OPTIONAL] Copy group permissions to other + #runwithfeedback \ + # "Copy group permissions to other" \ + # "chmod -R o=g ${_home_dir}" + + # [OPTIONAL] Copy user permissions to group + #runwithfeedback \ + # "Copy user permissions to group" \ + # "chmod -R g=u ${_home_dir}" + + # Set the setgid bit, so that files/folder under the directory + # will be created with the same group as + runwithfeedback \ + "Set the setgid bit to inherit folder permissions" \ + "chmod g+s '${_home_dir}'" + + # If Access Control Lists (ACL) is installed... + # To "activate" ACL, you have to remount the drive with the "acl" option + # NOTE: Btrfs and Xfs filesystem use the ACL mount option by default + # Example /etc/fstab Entry: + # UUID=abc123def456 / ext4 defaults,acl 0 1 + # Type "man acl" for more information + if hascommand --strict setfacl; then + # Use getfacl [directory] to check ACL for these directories + runwithfeedback \ + "Set user default ACL entries" \ + "setfacl -d -m u::rwx '${_home_dir}'" + runwithfeedback \ + "Set group default ACL entries" \ + "setfacl -d -m g::rx '${_home_dir}'" + runwithfeedback \ + "Set others default ACL entries" \ + "setfacl -d -m o::--- '${_home_dir}'" + fi + + echo "Done!" + else + return 0 + fi +} + +# Copy over configuration settings from one account to root/default/another +# It also handles special cases for 'root' and 'default' (skel) directories +# Syntax: +# configcopy [from_user] [to_user] +# Parameters: +# from_user - The user from whom to copy the configuration files +# to_user - The user to whom to copy the configuration files +# Examples: +# configcopy alice jimbob # Copies files from user alice's home to bob's home +# configcopy default root # Copies files from /etc/skel to /root +alias copyconfig='configcopy' +function configcopy() { + # Config files to copy over + declare -a files=( + ".bash_logout" + ".bash_profile" + ".bashrc" + ".bashrc_help" + ".commacd.sh" + ".config/alacritty/alacritty.toml" + ".config/alacritty/alacritty.yml" + ".config/bat/config" + ".config/btop/btop.conf" + ".config/dolphinrc" + ".config/fresh/config.json" + ".config/git/config" + ".config/git/ignore" + ".config/gtkrc" + ".config/helix/config.toml" + ".config/katerc" + ".config/kitty/kitty.conf" + ".config/konsolerc" + ".config/konsolesshconfig" + ".config/kwriterc" + ".config/lazygit/config.yml" + ".config/micro/bindings.json" + ".config/micro/settings.json" + ".config/Notepadqq/Notepadqq.ini" + ".config/nvim/init.lua" + ".config/nvim/init.vim" + ".config/picom/picom.conf" + ".config/starship.toml" + ".config/Typora/conf/conf.user.json" + ".config/Typora/themes/base.user.css" + ".config/Typora/themes/jeff.css" + ".config/Typora/themes/night.user.css" + ".config/VSCodium/User/settings.json" + ".config/yakuakerc" + ".config/yt-dlp/config" + ".curlrc" + ".dircolors" + ".editorconfig" + ".gitconfig" + ".gitignore_global" + ".gtkrc-2.0" + ".inputrc" + ".nanorc" + ".p10k.zsh" + ".profile" + ".ripgreprc" + ".screenrc" + ".selected_editor" + ".tmux.conf" + ".vimrc" + ".wgetrc" + ".Xmodmap" + ".xprofile" + ".Xresources" + ".zshrc" + "git-completion.bash" + "git-prompt.sh" + "gitalias.txt" + ) + + # Config directories to copy over + declare -a dirs=( + ".config/alacritty" + ".config/bashrc" + ".config/bashrc/.qfc" + ".config/bashrc/bashmarks" + ".config/bashrc/enhancd" + ".config/bashrc/fzf-tab-completion" + ".config/bashrc/hstr" + ".config/bat" + ".config/btop" + ".config/devilspie2" + ".config/fish" + ".config/flameshot" + ".config/fontconfig" + ".config/geany" + ".config/ghostty" + ".config/git" + ".config/gtk-3.0" + ".config/gtk-4.0" + ".config/htop" + ".config/kitty" + ".config/Kvantum" + ".config/lazygit" + ".config/lxterminal" + ".config/mc" + ".config/micro/colorschemes" + ".config/micro/plug" + ".config/mpv" + ".config/nvim" + ".config/qterminal.org" + ".config/ranger" + ".config/rofi" + ".config/sxhkd" + ".config/swhkd" + ".config/terminator" + ".config/wezterm" + ".config/yt-dlp" + ".local/share/blesh/src/ble.sh" + ".local/share/dolphin" + ".local/share/konsole" + ".local/share/kxmlgui5/dolphin" + ".local/share/kxmlgui5/konsole" + ".local/share/qtermwidget6/color-schemes" + ".qfc" + ".vim" + "bashmarks" + "ble.sh" + "enhancd" + "fzf-tab-completion" + "hstr" + ) + + # Declare routing variables as local to avoid polluting global scope + local dirfrom dirto owner + + if [[ -z "${1}" ]] || [[ -z "${2}" ]] || [[ "${1}" == "--help" ]] || [[ "${1}" == "-h" ]]; then + echo -e "${BRIGHT_CYAN}configcopy${RESET}: Copy config files between users" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}configcopy${RESET} ${BRIGHT_MAGENTA}<${BRIGHT_YELLOW}from_user${BRIGHT_MAGENTA}>${RESET} ${BRIGHT_MAGENTA}<${BRIGHT_YELLOW}to_user${BRIGHT_MAGENTA}>${RESET}" + echo -e "${BRIGHT_WHITE}Special users:${RESET} ${BRIGHT_CYAN}root${RESET}, ${BRIGHT_CYAN}default${RESET} (/etc/skel)" + echo -e "${BRIGHT_WHITE}Available users:${RESET} ${BRIGHT_CYAN}$(command awk -F: '$3 ~ /^[0-9]{4}$/' /etc/passwd | command cut -d: -f1 | command tr '\n' ' ' | command sed 's/[[:space:]]*$//')${RESET}" + echo -e "${BRIGHT_WHITE}Examples:${RESET}" + echo -e " ${BRIGHT_CYAN}configcopy${RESET} ${BRIGHT_YELLOW}alice bob${RESET} ${BRIGHT_BLUE}# Copy from alice to bob${RESET}" + echo -e " ${BRIGHT_CYAN}configcopy${RESET} ${BRIGHT_YELLOW}default root${RESET} ${BRIGHT_BLUE}# Copy /etc/skel to /root${RESET}" + echo -e " ${BRIGHT_CYAN}configcopy${RESET} ${BRIGHT_YELLOW}jeff default${RESET} ${BRIGHT_BLUE}# Update /etc/skel${RESET}" + return + elif [ "${1}" == "${2}" ]; then + echo -e "${BRIGHT_RED}Error: ${BRIGHT_CYAN}The from and to user parameters cannot be the same${RESET}" + return 2 + elif [ ! -d "/home/${1}" ] && [ "${1}" != "root" ] && [ "${1}" != "default" ]; then + echo -e "${BRIGHT_RED}Error: ${BRIGHT_CYAN}User ${BRIGHT_YELLOW}${1}${BRIGHT_CYAN} does not exist${RESET}" + return 1 + elif [ ! -d "/home/${2}" ] && [ "${2}" != "root" ] && [ "${2}" != "default" ]; then + echo -e "${BRIGHT_RED}Error: ${BRIGHT_CYAN}User ${BRIGHT_YELLOW}${2}${BRIGHT_CYAN} does not exist${RESET}" + return 1 + elif [ "${1}" == "default" ] && [ "${2}" == "root" ]; then + dirfrom=/etc/skel/ + dirto=/root/ + owner=root + elif [ "${1}" == "default" ]; then + dirfrom=/etc/skel/ + dirto=/home/"${2}"/ + owner="${2}" + elif [ "${2}" == "default" ] && [ "${1}" == "root" ]; then + dirfrom=/root/ + dirto=/etc/skel/ + owner=root + elif [ "${2}" == "default" ]; then + dirfrom=/home/"${1}"/ + dirto=/etc/skel/ + owner=root + elif [ "${1}" == "root" ]; then + dirfrom=/root/ + dirto=/home/"${2}"/ + owner="${2}" + elif [ "${2}" == "root" ]; then + dirfrom=/home/"${1}"/ + dirto=/root/ + owner=root + else + dirfrom=/home/"${1}"/ + dirto=/home/"${2}"/ + owner="${2}" + fi + + # Ask for confirmation + if ! ask "${BRIGHT_RED}Are you sure? ${BRIGHT_CYAN}This will overwrite configuration files in ${BRIGHT_YELLOW}${dirto%/}${RESET}" N; then + return + fi + + # Check if the user can execute sudo commands (only needed if copying to another user) + if [[ "${owner}" != "${USER}" ]] && ! sudo -v 2>/dev/null; then + echo -e "${BRIGHT_RED}Error:${RESET} You do not have sufficient permissions to run this command with sudo" + return 1 + fi + + # Loop through and copy the files over + for file in "${files[@]}"; do + src="${dirfrom}${file}" + dest="${dirto}${file}" + dest_dir=$(dirname "${dest}") + + # Check if the source file exists + if [[ -f "${src}" ]]; then + # Ensure destination directory exists + [[ ! -d "${dest_dir}" ]] && \ + sudo mkdir -p "${dest_dir}" && \ + sudo chown "${owner}":"${owner}" "${dest_dir}" + + # Copy and change permissions + sudo cp "${src}" "${dest_dir}" && \ + sudo chown "${owner}":"${owner}" "${dest}" && \ + echo -e "${BRIGHT_GREEN}✓${RESET} Copied file: ${BRIGHT_CYAN}${file}${RESET}" + fi + done + + # Loop through and copy directories over + for dir in "${dirs[@]}"; do + src="${dirfrom}${dir}" + dest="${dirto}${dir}" + dest_dir=$(dirname "${dest}") + + # Check if the source directory exists + if [[ -d "${src}" ]]; then + # Ensure destination directory exists + [[ ! -d "${dest_dir}" ]] && \ + sudo mkdir -p "${dest_dir}" && \ + sudo chown "${owner}":"${owner}" "${dest_dir}" + + # Copy directory and change ownership recursively + sudo cp -R "${src}" "${dest_dir}" && \ + sudo chown -R "${owner}":"${owner}" "${dest}" && \ + echo -e "${BRIGHT_GREEN}✓${RESET} Copied directory ${BRIGHT_CYAN}${dir}${RESET}" + fi + done + + # We are done + echo -e "Owner set to: ${BRIGHT_MAGENTA}${owner}${RESET}" + echo -e "${BRIGHT_GREEN}Finished copying configuration files from ${BRIGHT_YELLOW}${dirfrom%/}${BRIGHT_GREEN} to ${BRIGHT_YELLOW}${dirto%/}${RESET}" + return +} + +# Synchronize files using rsync over SSH +function sync2ssh() { + # Check the number of arguments + if [[ $# -lt 3 ]]; then + echo -e "${BRIGHT_WHITE}sync2ssh:${RESET} Synchronize files using rsync over SSH" + echo -e "${BRIGHT_WHITE}Usage:${RESET}" + echo -e " ${BRIGHT_CYAN}sync2ssh${RESET} ${BRIGHT_YELLOW}[LOCAL_DIR] ${BRIGHT_BLUE}[USER@REMOTE_HOST${BRIGHT_CYAN}[:PORT]${BRIGHT_BLUE}] ${BRIGHT_YELLOW}[REMOTE_DIR]${RESET} ${BRIGHT_GREEN}[OPTIONAL_SSH_PASSWORD]${RESET}" + echo -e "${BRIGHT_WHITE}Examples:${RESET}" + echo -e " ${BRIGHT_CYAN}sync2ssh${RESET} ${BRIGHT_YELLOW}~/local/folder/${RESET} ${BRIGHT_BLUE}user@example.com${RESET} ${BRIGHT_YELLOW}/path/on/remote${RESET}" + echo -e " ${BRIGHT_CYAN}sync2ssh${RESET} ${BRIGHT_YELLOW}~/local/folder/${RESET} ${BRIGHT_BLUE}user@example.com${RESET} ${BRIGHT_YELLOW}/path/on/remote${RESET} ${BRIGHT_GREEN}password123${RESET}" + echo -e " ${BRIGHT_CYAN}sync2ssh${RESET} ${BRIGHT_YELLOW}~/local/folder/${RESET} ${BRIGHT_BLUE}user@example.com${BRIGHT_WHITE}:${BRIGHT_CYAN}22${RESET} ${BRIGHT_YELLOW}/path/on/remote${RESET}" + return 1 + fi + + # Append trailing slash to directories if it's not present + local LOCAL_DIR + [[ "${1: -1}" != "/" ]] && LOCAL_DIR="${1}/" || LOCAL_DIR="$1" + local SSH_USER_HOST_PORT="$2" + local REMOTE_DIR="$3" + + # Extract the port (if present) + local SSH_PORT SSH_USER_HOST + SSH_PORT="${SSH_USER_HOST_PORT##*:}" + if [[ "${SSH_PORT}" == "${SSH_USER_HOST_PORT}" ]]; then + SSH_PORT=22 # default SSH port + SSH_USER_HOST="${SSH_USER_HOST_PORT}" + else + SSH_USER_HOST="${SSH_USER_HOST_PORT%%:*}" + fi + + # Build the rsync command as an array to avoid eval + # Rsync options: + # --recursive: Transfer files and directories recursively + # --links: Treat symbolic links as references to their target files/directories + # --compress: Compress files during transfer for efficiency + # --verbose: Display a detailed log of files being transferred + # --delete: Delete files on the destination that aren't present at the source + # -e: Use SSH with a 10-second connection timeout for transfers + local RSYNC_CMD=() + + # Check if SSH password is provided + if [[ -n "$4" ]]; then + # Ensure sshpass is installed + if ! hascommand --strict sshpass; then + echo -e "${BRIGHT_RED}Error: ${BRIGHT_CYAN}Install sshpass or use SSH keys instead${RESET}" + return 1 + fi + # Use SSHPASS env var instead of -p flag (avoids password in ps output) + RSYNC_CMD=(env "SSHPASS=$4" sshpass -e) + fi + + RSYNC_CMD+=(rsync --recursive --links --compress --verbose --delete + -e "ssh -o ConnectTimeout=10 -p ${SSH_PORT}" + "${LOCAL_DIR}" "${SSH_USER_HOST}:${REMOTE_DIR}") + + # Execute the rsync command directly (no eval needed) + if ! "${RSYNC_CMD[@]}"; then + echo -e "${BRIGHT_RED}Error: ${BRIGHT_CYAN}rsync command failed${RESET}" + return 1 + fi + + echo -e "${BRIGHT_GREEN}Files synchronized successfully${RESET}" +} + +# When executed, will toggle the hosts file on and off +function hoststoggle() { + # Help text + if [[ "${1}" == "--help" ]] || [[ "${1}" == "-h" ]]; then + echo -e "${BRIGHT_CYAN}hoststoggle${RESET}: Toggle hosts file between full and minimal" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}hoststoggle${RESET}" + echo -e "${BRIGHT_WHITE}Description:${RESET}" + echo -e " ${BRIGHT_BLUE}Backs up current hosts file and replaces with minimal version${RESET}" + echo -e " ${BRIGHT_BLUE}Run again to restore the backup${RESET}" + echo -e " ${BRIGHT_BLUE}Useful for testing if hosts blocking causes issues${RESET}" + return 0 + fi + + # Confirm we want to do this... + if ask "Are you sure you wish to modify the hosts file?" N; then + + # Check if the user can execute sudo commands + if ! sudo -v; then + echo -e "${BRIGHT_RED}Error:${RESET} You do not have sufficient permissions to run this script with necessary privileges" + return 1 + fi + + # If the hosts backup file exists, restore it + if [ -f /etc/hosts.backup_temporary ]; then + sudo \rm -f /etc/hosts + sudo \mv /etc/hosts.backup_temporary /etc/hosts + sudo chmod 644 /etc/hosts + echo -e "${BRIGHT_BLUE}Hosts file ${BRIGHT_GREEN}restored${BRIGHT_BLUE}.${RESET}" + echo -e "(File is $(sudo cat /etc/hosts | wc -l) lines long.)" + sudo head -16 /etc/hosts + + # Make a backup of the hosts file + elif [ -f /etc/hosts ]; then + sudo \mv /etc/hosts /etc/hosts.backup_temporary + + # Replaces the hosts file with a generic empty hosts file + sudo bash -c 'printf "#\n# /etc/hosts: static lookup table for host names\n#\n\n127.0.0.1 localhost\n255.255.255.255 broadcasthost\n::1 localhost\n::1 ip6-localhost ip6-loopback\nfe00::0 ip6-localnet\nff00::0 ip6-mcastprefix\nff02::1 ip6-allnodes\nff02::2 ip6-allrouters\nff02::3 ip6-allhosts\n" > /etc/hosts' + + sudo chmod 644 /etc/hosts + echo -e "${BRIGHT_BLUE}Hosts file ${BRIGHT_RED}disabled${BRIGHT_BLUE}.${RESET}" + + # Supports hBlock - A POSIX-compliant shell script that gets a list of domains + # that serve ads, tracking scripts and malware from multiple sources and creates + # a hosts file, among other formats, that prevents your system from connecting + # to them. See https://github.com/hectorm/hblock + elif hascommand --strict hblock; then + if [ -f /etc/hosts.personal ]; then + sudo /usr/bin/hblock --header /etc/hosts.personal + else + sudo /usr/bin/hblock + fi + sudo chmod 644 /etc/hosts + echo -e "${BRIGHT_BLUE}Hosts file ${BRIGHT_YELLOW}recreated${BRIGHT_BLUE}.${RESET}" + + # No hosts file was found + else + echo -e "${BRIGHT_RED}ERROR: ${BRIGHT_CYAN}Hosts file and backup not found.${RESET}" + fi + fi +} + +function timeelapsed() { + # Check if at least one parameter is provided + if [[ -z "${1}" ]]; then + echo -e "Please provide a date in ${BRIGHT_CYAN}YYYY-MM-DD${RESET} format." + return 1 + fi + + # Parameters + local DATE_GIVEN="${1}" + local MESSAGE="${2}" + + # Check if the first parameter is a valid date... + if ! date -d "${DATE_GIVEN}" >/dev/null 2>&1; then + echo -e "Invalid date format. Please provide a date in ${BRIGHT_CYAN}YYYY-MM-DD${RESET} format." + return 1 + fi + + # Default message if the second parameter is empty... + if [[ -z "${MESSAGE}" ]]; then + MESSAGE="${BRIGHT_CYAN}The date${RESET} ${BRIGHT_GREEN}${DATE_GIVEN}${RESET} ${BRIGHT_CYAN}was" + else + MESSAGE="${BRIGHT_CYAN}${MESSAGE}${RESET}" + fi + + # Calculate total days elapsed + local TOTAL_DAYS=$(( ( $(date '+%s') - $(date -d "${DATE_GIVEN}" '+%s') ) / 86400 )) + + # Use date arithmetic for accurate year/month/day breakdown + # (accounts for leap years and varying month lengths) + local GIVEN_Y GIVEN_M GIVEN_D NOW_Y NOW_M NOW_D + GIVEN_Y=$(date -d "${DATE_GIVEN}" '+%Y') + GIVEN_M=$(date -d "${DATE_GIVEN}" '+%-m') + GIVEN_D=$(date -d "${DATE_GIVEN}" '+%-d') + NOW_Y=$(date '+%Y') + NOW_M=$(date '+%-m') + NOW_D=$(date '+%-d') + + local YEARS=$((NOW_Y - GIVEN_Y)) + local MONTHS=$((NOW_M - GIVEN_M)) + local DAYS=$((NOW_D - GIVEN_D)) + + # Borrow from months if days went negative + if [[ ${DAYS} -lt 0 ]]; then + ((MONTHS--)) + # Days in previous month (use date to get it right) + DAYS=$((DAYS + $(date -d "${NOW_Y}-${NOW_M}-01 -1 day" '+%-d'))) + fi + + # Borrow from years if months went negative + if [[ ${MONTHS} -lt 0 ]]; then + ((YEARS--)) + MONTHS=$((MONTHS + 12)) + fi + + # Display message with colors + echo -e "${MESSAGE} ${BRIGHT_MAGENTA}${TOTAL_DAYS} total days${BRIGHT_CYAN} ago which is ${BRIGHT_YELLOW}${YEARS} years${BRIGHT_CYAN}, ${BRIGHT_YELLOW}${MONTHS} months${BRIGHT_CYAN}, and ${BRIGHT_YELLOW}${DAYS} days${BRIGHT_CYAN}!${RESET}" +} + +# ROT13 (rotate by 13 positions) is a simple letter substitution cipher that +# replaces each letter with the letter 13 positions after it in the alphabet +# NOTE: Since the English alphabet has 26 letters, ROT13 acts as its own inverse +# applying it twice gets you back to the original text. +function rot13() { + if [[ $# -eq 0 ]]; then + tr '[a-m][n-z][A-M][N-Z]' '[n-z][a-m][N-Z][A-M]' + else + echo "$*" | tr '[a-m][n-z][A-M][N-Z]' '[n-z][a-m][N-Z][A-M]' + fi +} + +# Command to spell check command line input +# Example: spellcheck "definately" +# Example: spellcheck "I sincerly reccomend programing" +spellcheck() { + + # Check if any spell checker is installed + if type -P aspell >/dev/null; then + local SPELLCHECKER="aspell -a" + elif type -P hunspell >/dev/null; then + local SPELLCHECKER="hunspell" + else + echo -e "${BRIGHT_RED}No spell checker found: ${BRIGHT_CYAN}Please install aspell or hunspell${RESET}" + return 1 + fi + + # Check if any arguments were provided + if [[ $# -eq 0 ]]; then + echo -e "${BRIGHT_WHITE}Usage: ${BRIGHT_GREEN}spellcheck ${BRIGHT_YELLOW}[text to check]${RESET}" + return 1 + fi + + # Combine all arguments into a single string + local TEXT="$*" + + # Run spell checker and process the output + local LINE + local WORD + local SUGGESTIONS + echo "${TEXT}" | ${SPELLCHECKER} | while read -r LINE; do + # Skip the version header line + if [[ ${LINE} == @\(#\)* || ${LINE} == Hunspell* ]]; then + continue + # Skip lines that just show '*' (correctly spelled words) + elif [[ ${LINE} == \** && ${#LINE} -eq 1 ]]; then + continue + # Process spell check suggestions + elif [[ ${LINE} == \&* ]]; then + # Extract the misspelled word and suggestions + WORD=$(echo "${LINE}" | cut -d' ' -f2) + # Get everything after the ':' or last number for suggestions + if [[ ${SPELLCHECKER} == "hunspell" ]]; then + # Hunspell format: "& word num offset: suggestion1, suggestion2" + SUGGESTIONS=$(echo "${LINE}" | cut -d':' -f2- | sed 's/^[ ]*//') + # If no colon, get everything after the last number + if [[ -z "${SUGGESTIONS}" ]]; then + SUGGESTIONS=$(echo "${LINE}" | sed 's/^& [^ ]* [0-9]* [0-9]* //') + fi + else + # Aspell format + SUGGESTIONS=$(echo "${LINE}" | cut -d':' -f2- | sed 's/^[ ]*//') + fi + + echo -e "${BRIGHT_RED}X Misspelled: ${BRIGHT_CYAN}${WORD}${RESET}" + echo -e "${BRIGHT_GREEN}✓ Suggestions:${RESET} ${BRIGHT_YELLOW}${SUGGESTIONS}${RESET}" + echo + fi + done +} + +# Transform text using common string formatting and case operations +function formattext() { + local TEXT="" + local PARAMS=() + + # Internal function to show help message + # Named with function prefix to avoid polluting global scope + # (bash doesn't scope nested functions — they become global) + function _formattext_show_help() { + echo -e "${BRIGHT_CYAN}formattext${RESET}: Format text with various transformations" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}formattext${RESET} ${BRIGHT_MAGENTA}[${BRIGHT_GREEN}options${BRIGHT_MAGENTA}]${RESET} ${BRIGHT_MAGENTA}<${BRIGHT_YELLOW}text${BRIGHT_MAGENTA}>${RESET}" + echo -e " command | ${BRIGHT_CYAN}formattext${RESET} ${BRIGHT_MAGENTA}[${BRIGHT_GREEN}options${BRIGHT_MAGENTA}]${RESET}" + echo -e "${BRIGHT_WHITE}Note:${RESET} Text is processed in order of options specified" + echo -e "${BRIGHT_WHITE}Options:${RESET}" + echo -e " ${BRIGHT_GREEN}-c${RESET}, ${BRIGHT_GREEN}--capitalize${RESET} Capitalize every word" + echo -e " ${BRIGHT_GREEN}-C${RESET}, ${BRIGHT_GREEN}--titlecase${RESET} Convert text to title case" + echo -e " ${BRIGHT_GREEN}-d${RESET}, ${BRIGHT_GREEN}--dashes-to-spaces${RESET} Replace dashes with spaces" + echo -e " ${BRIGHT_GREEN}-f${RESET}, ${BRIGHT_GREEN}--filename-friendly${RESET} Replace characters difficult for filenames" + echo -e " ${BRIGHT_GREEN}-i${RESET}, ${BRIGHT_GREEN}--input${RESET} Specify input text (-i=text or -i text)" + echo -e " ${BRIGHT_GREEN}-l${RESET}, ${BRIGHT_GREEN}--lowercase${RESET} Convert text to lowercase" + echo -e " ${BRIGHT_GREEN}-q${RESET}, ${BRIGHT_GREEN}--smart-quotes${RESET} Convert quotes to smart quotes" + echo -e " ${BRIGHT_GREEN}-Q${RESET}, ${BRIGHT_GREEN}--unsmart-quotes${RESET} Smart quotes to regular quotes" + echo -e " ${BRIGHT_GREEN}-r${RESET}, ${BRIGHT_GREEN}--remove-duplicate-spaces${RESET} Remove duplicate spaces" + echo -e " ${BRIGHT_GREEN}-s${RESET}, ${BRIGHT_GREEN}--spaces-to-dashes${RESET} Replace spaces with dashes" + echo -e " ${BRIGHT_GREEN}-S${RESET}, ${BRIGHT_GREEN}--spaces-to-underscores${RESET} Replace spaces with underscores" + echo -e " ${BRIGHT_GREEN}-t${RESET}, ${BRIGHT_GREEN}--trim${RESET} Trim leading and trailing whitespace" + echo -e " ${BRIGHT_GREEN}-T${RESET}, ${BRIGHT_GREEN}--tabs-to-spaces${RESET} Replace tabs with spaces" + echo -e " ${BRIGHT_GREEN}-u${RESET}, ${BRIGHT_GREEN}--uppercase${RESET} Convert text to uppercase" + echo -e " ${BRIGHT_GREEN}-U${RESET}, ${BRIGHT_GREEN}--underscores-to-spaces${RESET} Replace underscores with spaces" + echo -e "${BRIGHT_WHITE}Examples:${RESET}" + echo -e " ${BRIGHT_CYAN}formattext${RESET} ${BRIGHT_GREEN}--titlecase${RESET} ${BRIGHT_YELLOW}'hello world'${RESET} ${BRIGHT_BLUE}# Returns: Hello World${RESET}" + echo -e " ${BRIGHT_CYAN}formattext${RESET} ${BRIGHT_GREEN}-r -C${RESET} ${BRIGHT_YELLOW}'hello world'${RESET} ${BRIGHT_BLUE}# Combine options${RESET}" + echo -e " ${BRIGHT_YELLOW}echo 'text'${RESET} | ${BRIGHT_CYAN}formattext${RESET} ${BRIGHT_GREEN}-u${RESET} ${BRIGHT_BLUE}# Uppercase from pipe${RESET}" + return 0 + } + + # Check for piped input + local LINE + if [[ ! -t 0 ]]; then + # Read from stdin (pipe) line by line + while IFS= read -r LINE; do + if [[ -z "${TEXT}" ]]; then + TEXT="${LINE}" + else + TEXT="${TEXT}"$'\n'"${LINE}" + fi + done + + # Else check to make sure we have parameters + elif [[ $# -eq 0 ]]; then + # Show help if no parameters provided + _formattext_show_help + return 0 + fi + + # Parse command line options + while [[ $# -gt 0 ]]; do + case "${1}" in + -h|--help) + _formattext_show_help + return 0 + ;; + -i=*|--input=*) + if [[ -z "${TEXT}" ]]; then + TEXT="${1#*=}" # If text is empty, set it + else + TEXT+=" ${1#*=}" # Otherwise append with space + fi + shift + ;; + -i|--input) + if [[ -n "${2}" ]] && [[ "${2}" != -* ]]; then + if [[ -z "${TEXT}" ]]; then + TEXT="${2}" # If text is empty, set it + else + TEXT+=" ${2}" # Otherwise append with space + fi + shift 2 + else + echo "Error: --input requires an argument" >&2 + return 1 + fi + ;; + -*) + PARAMS+=("${1}") + shift + ;; + *) + if [[ -z "${TEXT}" ]]; then + TEXT="${1}" # First word without space + else + TEXT+=" ${1}" # Subsequent words with space + fi + shift + ;; + esac + done + + # Check if no text is provided + if [[ -z "${TEXT}" ]]; then + return 1 + fi + + # Apply transformations in the specified order + local PARAM + local RESULT + local SMALL_WORDS + local CAPITALIZE_NEXT + local WORD + local INDEX + local CHAR + local LEFT_DOUBLE_QUOTE + local RIGHT_DOUBLE_QUOTE + local LEFT_SINGLE_QUOTE + local RIGHT_SINGLE_QUOTE + for PARAM in "${PARAMS[@]}"; do + case "${PARAM}" in + -c|--capitalize) + TEXT=$(IFS=' ' read -ra words <<< "${TEXT}" && (IFS=' '; echo "${words[@]^}")) + ;; + -C|--titlecase) + RESULT="" + SMALL_WORDS="a|an|and|as|at|but|by|en|for|if|in|nor|of|on|or|per|the|to|vs?\\.?|via" + CAPITALIZE_NEXT=true + for WORD in ${TEXT}; do + if ${CAPITALIZE_NEXT} || ! [[ ${WORD} =~ ^(${SMALL_WORDS})$ ]]; then + RESULT+="${WORD^} " + CAPITALIZE_NEXT=false + else + RESULT+="${WORD,,} " + fi + # Check if we should capitalize the next word + if [[ ${WORD: -1} =~ [:.?!-] ]]; then + CAPITALIZE_NEXT=true + fi + done + TEXT="${RESULT% }" + ;; + -d|--dashes-to-spaces) + TEXT="${TEXT//-/ }" + ;; + -f|--filename-friendly) + RESULT="" + for ((INDEX=0; INDEX<${#TEXT}; INDEX++)); do + CHAR="${TEXT:INDEX:1}" + case "${CHAR}" in + "<") RESULT+="<" ;; + ">") RESULT+=">" ;; + ":") RESULT+=":" ;; + "\"") RESULT+=""" ;; + "/") RESULT+="/" ;; + "\\") RESULT+="\" ;; + "|") RESULT+="|" ;; + "?") RESULT+="?" ;; + "*") RESULT+="*" ;; + [[:cntrl:]]) RESULT+=" " ;; + *) RESULT+="${CHAR}" ;; + esac + done + TEXT="${RESULT}" + ;; + -l|--lowercase) + TEXT="${TEXT,,}" + ;; + -q|--smart-quotes) + if ! hascommand sed; then + echo -e "${BRIGHT_RED}Error:${RESET} ${BRIGHT_CYAN}Smart quotes requires ${BRIGHT_YELLOW}sed${RESET}" + return 1 + fi + + # Get the actual Unicode characters for smart quotes + LEFT_DOUBLE_QUOTE=$(echo -ne '\u201C') # Left double quote " + RIGHT_DOUBLE_QUOTE=$(echo -ne '\u201D') # Right double quote " + LEFT_SINGLE_QUOTE=$(echo -ne '\u2018') # Left single quote ' + RIGHT_SINGLE_QUOTE=$(echo -ne '\u2019') # Right single quote ' + + # First convert doubles + TEXT=$(echo "${TEXT}" | sed -e "s/\"/${LEFT_DOUBLE_QUOTE}/g") # Replace all " with opening " + TEXT=$(echo "${TEXT}" | sed -e "s/${LEFT_DOUBLE_QUOTE}\([^${RIGHT_DOUBLE_QUOTE}]*\)$/${LEFT_DOUBLE_QUOTE}\1/g") # Fix lone opening quotes at end + TEXT=$(echo "${TEXT}" | sed -e "s/${LEFT_DOUBLE_QUOTE}\([^${RIGHT_DOUBLE_QUOTE}]*\)${LEFT_DOUBLE_QUOTE}/${LEFT_DOUBLE_QUOTE}\1${RIGHT_DOUBLE_QUOTE}/g") # Fix pairs + + # Then convert singles + TEXT=$(echo "${TEXT}" | sed -e "s/'/${LEFT_SINGLE_QUOTE}/g") # Replace all ' with opening ' + TEXT=$(echo "${TEXT}" | sed -e "s/${LEFT_SINGLE_QUOTE}\([^${RIGHT_SINGLE_QUOTE}]*\)$/${LEFT_SINGLE_QUOTE}\1/g") # Fix lone opening quotes at end + TEXT=$(echo "${TEXT}" | sed -e "s/${LEFT_SINGLE_QUOTE}\([^${RIGHT_SINGLE_QUOTE}]*\)${LEFT_SINGLE_QUOTE}/${LEFT_SINGLE_QUOTE}\1${RIGHT_SINGLE_QUOTE}/g") # Fix pairs + ;; + -Q|--unsmart-quotes) + # Convert from smart quotes back to regular + TEXT="${TEXT//'/\'}" + TEXT="${TEXT//'/\'}" + TEXT="${TEXT//\"/\"}" + TEXT="${TEXT//\"/\"}" + ;; + -r|--remove-duplicate-spaces) + while [[ "${TEXT}" =~ " " ]]; do + TEXT="${TEXT// / }" + done + ;; + -s|--spaces-to-dashes) + # First remove duplicate spaces, then convert to dashes + while [[ "${TEXT}" =~ " " ]]; do + TEXT="${TEXT// / }" + done + TEXT="${TEXT// /-}" + ;; + -S|--spaces-to-underscores) + TEXT="${TEXT// /_}" + ;; + -t|--trim) + TEXT="${TEXT#"${TEXT%%[![:space:]]*}"}" # trim leading + TEXT="${TEXT%"${TEXT##*[![:space:]]}"}" # trim trailing + ;; + -T|--tabs-to-spaces) + TEXT="${TEXT// / }" + ;; + -u|--uppercase) + TEXT="${TEXT^^}" + ;; + -U|--underscores-to-spaces) + TEXT="${TEXT//_/ }" + ;; + esac + done + + echo "${TEXT}" +} + +# Convenience aliases for common text formatting operations +# These support both direct input: lowercase "HELLO" +# and piped input: echo "HELLO" | lowercase +# NOTE: Aliases only work in interactive shells, not in scripts +alias trim='formattext --trim' +alias fixspaces='formattext --tabs-to-spaces --underscores-to-spaces --remove-duplicate-spaces' +alias lowercase='formattext --lowercase' +alias uppercase='formattext --uppercase' +alias capitalize='formattext --capitalize' +alias titlecase='formattext --titlecase' +alias smartquotes='formattext --smart-quotes' +alias unsmartquotes='formattext --unsmart-quotes' + +# Reverse the text in a string +function reversetext() { + if [[ "${1}" == "--help" ]] || [[ "${1}" == "-h" ]] || { [[ $# -eq 0 ]] && [[ -t 0 ]]; }; then + echo -e "${BRIGHT_CYAN}reversetext${RESET}: Reverse the characters in text" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}reversetext${RESET} ${BRIGHT_MAGENTA}<${BRIGHT_YELLOW}text${BRIGHT_MAGENTA}>${RESET}" + echo -e " command | ${BRIGHT_CYAN}reversetext${RESET}" + echo -e "${BRIGHT_WHITE}Examples:${RESET}" + echo -e " ${BRIGHT_CYAN}reversetext${RESET} ${BRIGHT_YELLOW}\"hello world\"${RESET} ${BRIGHT_BLUE}# Returns: dlrow olleh${RESET}" + echo -e " ${BRIGHT_YELLOW}echo \"racecar\"${RESET} | ${BRIGHT_CYAN}reversetext${RESET} ${BRIGHT_BLUE}# Returns: racecar${RESET}" + echo -e " ${BRIGHT_YELLOW}cat file.txt${RESET} | ${BRIGHT_CYAN}reversetext${RESET} ${BRIGHT_BLUE}# Reverse file contents${RESET}" + return 2 + fi + + local TEXT="" + local REVERSED="" + local LINE + local INDEX + + # Check for piped input + if [[ ! -t 0 ]]; then + # Read from stdin (pipe) line by line + while IFS= read -r LINE; do + if [[ -z "${TEXT}" ]]; then + TEXT="${LINE}" + else + TEXT="${TEXT}"$'\n'"${LINE}" + fi + done + else + TEXT="$*" + fi + + # Iterate over each character in the string in reverse order + for (( INDEX=${#TEXT}-1; INDEX>=0; INDEX-- )); do + REVERSED+="${TEXT:INDEX:1}" + done + + echo "${REVERSED}" +} + +# Count the number of characters in a string +function countchars() { + # Help text + if [[ "${1}" == "--help" ]] || [[ "${1}" == "-h" ]]; then + echo -e "${BRIGHT_CYAN}countchars${RESET}: Count characters in a string" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}countchars${RESET} ${BRIGHT_MAGENTA}<${BRIGHT_YELLOW}text${BRIGHT_MAGENTA}>${RESET}" + echo -e "${BRIGHT_WHITE}Examples:${RESET}" + echo -e " ${BRIGHT_CYAN}countchars${RESET} ${BRIGHT_YELLOW}'hello world'${RESET} ${BRIGHT_BLUE}# Returns: 11${RESET}" + return 0 + fi + + # Use wc -m to count the number of characters (-n prevents trailing newline) + echo -n "$1" | wc -m +} + +# Count the number of words in a string +function countwords() { + # Help text + if [[ "${1}" == "--help" ]] || [[ "${1}" == "-h" ]]; then + echo -e "${BRIGHT_CYAN}countwords${RESET}: Count words in a string" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}countwords${RESET} ${BRIGHT_MAGENTA}<${BRIGHT_YELLOW}text${BRIGHT_MAGENTA}>${RESET}" + echo -e "${BRIGHT_WHITE}Examples:${RESET}" + echo -e " ${BRIGHT_CYAN}countwords${RESET} ${BRIGHT_YELLOW}'hello world'${RESET} ${BRIGHT_BLUE}# Returns: 2${RESET}" + return 0 + fi + + # Use wc -w to count the number of words (use $* to count all arguments) + echo "$*" | wc -w +} + +# Check if a string starts with a specific substring +function startswith() { + # Help text + if [[ "${1}" == "--help" ]] || [[ "${1}" == "-h" ]]; then + echo -e "${BRIGHT_CYAN}startswith${RESET}: Check if string starts with a prefix" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}startswith${RESET} ${BRIGHT_MAGENTA}<${BRIGHT_YELLOW}string${BRIGHT_MAGENTA}>${RESET} ${BRIGHT_MAGENTA}<${BRIGHT_YELLOW}prefix${BRIGHT_MAGENTA}>${RESET}" + echo -e "${BRIGHT_WHITE}Returns:${RESET} 0 if true, 1 if false" + echo -e "${BRIGHT_WHITE}Examples:${RESET}" + echo -e " ${BRIGHT_CYAN}startswith${RESET} ${BRIGHT_YELLOW}'hello' 'he'${RESET} && echo yes ${BRIGHT_BLUE}# Yes${RESET}" + echo -e " ${BRIGHT_CYAN}startswith${RESET} ${BRIGHT_YELLOW}'hello' 'lo'${RESET} || echo no ${BRIGHT_BLUE}# No${RESET}" + return 0 + fi + + # Use parameter expansion to check if the string starts with the substring + if [[ "$1" == "$2"* ]]; then + return 0 + else + return 1 + fi +} + +# Check if a string ends with a specific substring +function endswith() { + # Help text + if [[ "${1}" == "--help" ]] || [[ "${1}" == "-h" ]]; then + echo -e "${BRIGHT_CYAN}endswith${RESET}: Check if string ends with a suffix" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}endswith${RESET} ${BRIGHT_MAGENTA}<${BRIGHT_YELLOW}string${BRIGHT_MAGENTA}>${RESET} ${BRIGHT_MAGENTA}<${BRIGHT_YELLOW}suffix${BRIGHT_MAGENTA}>${RESET}" + echo -e "${BRIGHT_WHITE}Returns:${RESET} 0 if true, 1 if false" + echo -e "${BRIGHT_WHITE}Examples:${RESET}" + echo -e " ${BRIGHT_CYAN}endswith${RESET} ${BRIGHT_YELLOW}'hello' 'lo'${RESET} && echo yes ${BRIGHT_BLUE}# Yes${RESET}" + echo -e " ${BRIGHT_CYAN}endswith${RESET} ${BRIGHT_YELLOW}'hello' 'he'${RESET} || echo no ${BRIGHT_BLUE}# No${RESET}" + return 0 + fi + + # Use parameter expansion to check if the string ends with the substring + if [[ "$1" == *"$2" ]]; then + return 0 + else + return 1 + fi +} + +# Fix specified filenames by trimming whitespace, replacing multiple spaces, +# tabs, and underscores with a single space, and converting to title case +function fixfilename() { + local FILE + local DIRECTORY + local FILENAME + local NEW_FILENAME + local NEW_FILEPATH + for FILE in "$@"; do + if [[ -f "${FILE}" ]]; then + DIRECTORY=$(dirname "${FILE}") + FILENAME=$(basename "${FILE}") + + # Use formattext with multiple transformations: + NEW_FILENAME=$(formattext \ + --tabs-to-spaces \ + --underscores-to-spaces \ + --titlecase \ + --filename-friendly \ + --remove-duplicate-spaces \ + --trim \ + --input="${FILENAME##-}") + + if [[ "${FILENAME}" != "${NEW_FILENAME}" ]]; then + NEW_FILEPATH="${DIRECTORY}/${NEW_FILENAME}" + echo -e "${BRIGHT_CYAN}Rename: ${BRIGHT_YELLOW}${FILENAME}${RESET}" + echo -e "${BRIGHT_CYAN} To: ${BRIGHT_YELLOW}${NEW_FILENAME}${RESET}" + if ask "${BRIGHT_WHITE}Continue?${RESET}" Y; then + mv -i "${FILE}" "${NEW_FILEPATH}" + echo -e "${BRIGHT_GREEN}Renamed to ${BRIGHT_YELLOW}${NEW_FILENAME}${RESET}" + else + echo -e "${BRIGHT_CYAN}Skipped renaming ${BRIGHT_YELLOW}${FILENAME}${RESET}" + fi + else + echo -e "${BRIGHT_MAGENTA}No changes needed for ${BRIGHT_YELLOW}${FILENAME}${RESET}" + fi + else + echo -e "${BRIGHT_RED}File not found: ${BRIGHT_YELLOW}${FILE}${RESET}" + fi + done +} + +####################################################### +# Show the initial information HUD on initial Bash load +####################################################### + +# If we are NOT root or in a virtual terminal console or TMUX or Git Bash... +if [[ $EUID -ne 0 ]] && \ + [[ ! "$(tty)" =~ /dev/tty ]] && \ + [[ ! "${TERM}" =~ screen ]] && \ + [[ -z "${TMUX}" ]] && \ + [[ "${_KERNEL_NAME}" != "MINGW" ]] && \ + [[ "${_KERNEL_NAME}" != "CYGWI" ]] && \ + [[ -z "${INSIDE_EMACS}" ]] && \ + [[ -z "${VIM_TERMINAL}" ]] && \ + [[ ! "${TERM}" =~ vim ]] && \ + [[ ! "${TERM}" =~ nvim ]] && \ + [[ "${TERM_PROGRAM}" != "vscode" ]] && \ + [[ "${TERMINAL_EMULATOR}" != "JetBrains-JediTerm" ]] && \ + [[ "${TERM_PROGRAM}" != "Sublime_Terminal" ]] && \ + [[ "${TERM_PROGRAM}" != "atom" ]] && \ + [[ $_SKIP_SYSTEM_INFO = false ]]; then + + if hascommand --strict hyfetch; then + hyfetch + + # Link: https://github.com/LinusDierheimer/fastfetch + elif hascommand --strict fastfetch; then + fastfetch + + # Link: https://ostechnix.com/neofetch-display-linux-systems-information/ + elif hascommand --strict neofetch; then + neofetch + printf '\033[A\033[K' # Move the cursor up one line + + # Link: https://github.com/KittyKatt/screenFetch + elif hascommand --strict screenfetch; then + screenfetch + + # Link: https://github.com/deater/linux_logo + elif hascommand --strict linuxlogo; then + linuxlogo + + # Link: https://lclarkmichalek.github.io/archey3/ + elif hascommand --strict archey; then + archey + + # Link: https://github.com/dylanaraps/pfetch + elif [[ -f "${HOME}/pfetch.sh" ]]; then + "${HOME}/pfetch.sh" + elif hascommand --strict pfetch.sh; then + pfetch.sh + elif hascommand --strict pfetch; then + pfetch + fi +fi + +####################################################### +# Show text or ASCII on initial Bash load +# Create ASCII: jp2a --color ~/input_image.jpg > ~/.bash_motd_shown +# Test: cat ~/.bash_motd_shown +####################################################### + +# If the file exists and we are NOT root... +if [[ -f "${HOME}/.bash_motd_shown" ]] && [[ $EUID -ne 0 ]]; then + + # Show the ASCII text or image + cat "${HOME}/.bash_motd_shown" +fi + +####################################################### +# Show upgrade information on new terminal windows +# Note: This is disabled by default because it takes several seconds to run +####################################################### + +if [[ $_SKIP_UPGRADE_NOTIFY = false ]]; then + + # If this is an Arch based distrobution... + # Add to your crontab (this runs every 3 hours): 0 */3 * * * /usr/bin/pacman -Sy + if hascommand --strict pacman && hascommand --strict paccache; then + + # Show if there are updates available + _PACKAGE_UPDATE_COUNT=$(pacman -Q --upgrades | wc -l) + if [[ "$_PACKAGE_UPDATE_COUNT" -gt "0" ]]; then + echo -e "${BRIGHT_YELLOW}*${RESET} ${BRIGHT_GREEN}There are${RESET} ${BRIGHT_WHITE}"$_PACKAGE_UPDATE_COUNT"${RESET} ${BRIGHT_GREEN}avaliable program updates${RESET}" + echo -e "${BRIGHT_BLACK}*${RESET} ${BRIGHT_CYAN}Type${RESET} ${BRIGHT_MAGENTA}pacman -Qu${RESET} ${BRIGHT_CYAN}for more information${RESET}" + echo + fi + + # If this is an Ubuntu based distro... + # Add to your crontab (this runs every 3 hours): 0 */3 * * * /usr/bin/apt update + elif [[ -x "/usr/lib/update-notifier/apt-check" ]]; then + + # Show if there are updates available + IFS=';' read _PACKAGE_UPDATE_COUNT _PACKAGE_SECURITY_UPDATE_COUNT < <(/usr/lib/update-notifier/apt-check 2>&1) + if [[ "$_PACKAGE_UPDATE_COUNT" -gt "0" ]]; then + echo -e "${BRIGHT_YELLOW}*${RESET} ${BRIGHT_GREEN}There are${RESET} ${BRIGHT_WHITE}"$_PACKAGE_UPDATE_COUNT"${RESET} ${BRIGHT_GREEN}avaliable program updates${RESET}" + fi + if [[ "$_PACKAGE_SECURITY_UPDATE_COUNT" -gt "0" ]]; then + echo -e "${BRIGHT_BLACK}*${RESET} ${BRIGHT_CYAN}There are${RESET} ${BRIGHT_MAGENTA}"$_PACKAGE_SECURITY_UPDATE_COUNT"${RESET} ${BRIGHT_CYAN}security updates.${RESET}" + fi + if [[ "$_PACKAGE_UPDATE_COUNT" -gt "0" ]] || [[ "$_PACKAGE_SECURITY_UPDATE_COUNT" -gt "0" ]]; then + echo + fi + fi +fi + +################################################################################ +# Birthday/anniversary reminder that shows a message in your teminal +# Reads the birthday CSV file: ~/.config/birthdays.csv +# The format is (year is optional and can be left blank): +# Month,Day,Year,"Message" +# Jan,1,2000,"This is a message!" +# +# Toilet application is a required dependency +# Arch/Manjaro: sudo pacman -S toilet +# Ubuntu/Debian: sudo apt-get install toilet +################################################################################ + +# Make an alias to edit the birthday csv file +if [[ -f "${XDG_CONFIG_HOME:-${HOME}/.config}/birthdays.csv" ]]; then + alias ebd="edit ~/.config/birthdays.csv" +elif [[ -f "${_BDAY_FILE}" ]]; then + alias ebd="edit ${_BDAY_FILE}" +fi + +# Show a birthday or anniversary reminder +function birthday() { + # Help text + if [[ "${1}" == "--help" ]] || [[ "${1}" == "-h" ]]; then + echo -e "${BRIGHT_CYAN}birthday${RESET}: Display birthday reminders from CSV file" + echo -e "${BRIGHT_WHITE}Usage:${RESET} ${BRIGHT_CYAN}birthday${RESET}" + echo -e "${BRIGHT_WHITE}Description:${RESET}" + echo -e " ${BRIGHT_BLUE}Reads from ~/.config/birthdays.csv${RESET}" + echo -e " ${BRIGHT_BLUE}Format: Month,Day,Year,\"Message\"${RESET}" + echo -e " ${BRIGHT_BLUE}Example: Jan,15,1990,\"Happy Birthday John!\"${RESET}" + return 0 + fi + + # Use extended globbing for more advanced pattern matching + # This is also necessary for programmable completion + shopt -s extglob + + # If the birthday CSV file exists and toilet is installed... + if [[ $_SKIP_BDAY_REMINDER = false ]] && [[ -f "${_BDAY_FILE}" ]]; then + + # Loop through the birthday CSV file: ~/.config/birthdays.csv + # The first line is ignored (header) and the format is: + # Month,Day,Year,"Message" + # Jan,1,1985,"This is a message!" + while IFS=, read -r _BDAY_MONTH _BDAY_DAY _BDAY_YEAR _BDAY_MESSAGE; do + + # Compare the month (case insensitive) and the day (remove leading zeros) + if [[ "$(date +%^b)" = "${_BDAY_MONTH^^}" ]] && [[ "$(date +%-d)" = "${_BDAY_DAY##+(0)}" ]]; then + + # Remove the surrounding quotes from the message + _BDAY_MESSAGE="${_BDAY_MESSAGE%\"}" + _BDAY_MESSAGE="${_BDAY_MESSAGE#\"}" + + # Show the message using toilet and the future font + if hascommand --strict toilet; then + toilet --font="${_BDAY_FONT}" --termwidth --gay "${_BDAY_MESSAGE}" + elif hascommand --strict lolcat; then + echo "${_BDAY_MESSAGE}" | lolcat --spread=0.4 + else + echo -e "${BRIGHT_CYAN}${_BDAY_MESSAGE}${RESET}" + fi + + # If the year is a number and not blank, show the age + # Use date subtraction for accuracy (accounts for leap years) + if [ -n "${_BDAY_YEAR}" ] && [ "${_BDAY_YEAR}" -eq "${_BDAY_YEAR}" ] 2>/dev/null; then + echo -e "${BRIGHT_YELLOW}$(( $(date +%Y) - _BDAY_YEAR ))${RESET} Years" + fi + + #echo "" + fi + + # Strip off the first line in the CSV file + done < <(tail -n +2 "${_BDAY_FILE}") + fi +} + +# If we aren't supposed to skip this... +if [[ $_SKIP_BDAY_REMINDER = false ]]; then + # Call the birthday function + birthday +fi + +####################################################### +# Show the calendar when Bash starts +####################################################### + +if [[ $_SHOW_BASH_CALENDAR = true ]]; then + if hascommand --strict gcal; then + gcal -H '\033[34m:\033[0m:\033[32m:\033[0m' -q "${_GCAL_COUNTRY_CODE}" + else + cal -3 + fi +fi + +####################################################### +# bashmarks Directory Bookmarks +# Link: https://github.com/huyng/bashmarks +# Install: git clone git://github.com/huyng/bashmarks.git +# s - Saves the current directory as "bookmark_name" +# g - Goes (cd) to the directory associated with "bookmark_name" +# p - Prints the directory associated with "bookmark_name" +# d - Deletes the bookmark +# l - Lists all available bookmarks +####################################################### + +# If bashmarks is installed, load it +if [[ -f "${HOME}/bashmarks/bashmarks.sh" ]]; then + builtin source "${HOME}/bashmarks/bashmarks.sh" +elif [[ -f "${BASHRC_INSTALL_DIR}/bashmarks/bashmarks.sh" ]]; then + builtin source "${BASHRC_INSTALL_DIR}/bashmarks/bashmarks.sh" +elif [[ -f "${HOME}/.local/bin/bashmarks.sh" ]]; then + builtin source "${HOME}/.local/bin/bashmarks.sh" +elif [[ -f /usr/share/bashmarks/bashmarks.sh ]]; then + builtin source /usr/share/bashmarks/bashmarks.sh +fi + +####################################################### +# Zoxide is a smarter cd command (inspired by z and autojump) +# Link: https://github.com/ajeetdsouza/zoxide +# Install: curl -sS https://webinstall.dev/zoxide | bash +####################################################### + +# If Zoxide is installed, run it's initialization +if hascommand --strict zoxide; then + eval "$(zoxide init bash)" +fi + +####################################################### +# commacd Improved cd +# Link: https://github.com/shyiko/commacd +# Install: curl -sSL https://github.com/shyiko/commacd/raw/v1.0.0/commacd.sh -o ~/.commacd.sh +####################################################### + +# If commacd is installed +if [[ -f "${HOME}/.commacd.sh" ]]; then + builtin source "${HOME}/.commacd.sh" +elif [[ -f /usr/share/commacd/commacd.bash ]]; then + builtin source /usr/share/commacd/commacd.bash +elif [[ -f /usr/share/commacd/commacd.sh ]]; then + builtin source /usr/share/commacd/commacd.sh +fi + +####################################################### +# Improve navigation and searching your command history +####################################################### + +# Search command line history +alias h='history | grep' + +# Enable incremental history search with up/down arrows and other Readline features +# Learn more about this here: http://codeinthehole.com/writing/the-most-important-command-line-tip-incremental-history-searching-with-inputrc/ +bind '"\033[A": history-search-backward' +bind '"\033[B": history-search-forward' + +# Atuin - Magical shell history stored in a SQLite database +# (records command, directory, exit code, duration, hostname, and timestamp with full-text, fuzzy, or prefix search) +# Link: https://github.com/atuinsh/atuin +# Docs: https://docs.atuin.sh +# Install: sudo pacman -S atuin bash-preexec +if hascommand --strict atuin; then + + # Alias hh for atuin interactive search + alias hh='atuin search -i' + + # Initialize atuin (keep up/down arrows for history-search-backward/forward) + eval "$(atuin init bash --disable-up-arrow)" + +# HSTR Easily navigate and search your command history and favorites +# (has favorites, syncs across shells, does not require an extra database, but does not show the time since a command) +# Link: https://github.com/dvorka/hstr +# Manual: man hstr +elif hascommand --strict hstr; then + + # Alias hh for hstr + alias hh='hstr' + + # Get more colors + export HSTR_CONFIG=hicolor + + # Bind hstr to CTRL+r (for Vi mode check doc) + bind '"\C-r": "\C-a hstr -- \C-j"' + + # Bind 'kill last command' to CTRL+x k + bind '"\C-xk": "\C-a hstr -k \C-j"' + +# McFly - fly through your shell history using a small neural network +# (shows the time since the command, but does not have favorites, and has issues syncing history across multiple shells) +# NOTE: You can type % to match any number of characters when searching +# Link: https://github.com/cantino/mcfly +# Install: curl -LSfs https://raw.githubusercontent.com/cantino/mcfly/master/ci/install.sh | sh -s -- --git cantino/mcfly +elif hascommand --strict mcfly; then + + # Initialize McFly + eval "$(mcfly init bash)" + + # Alias hh for McFly + alias hh='mcfly search' + + # Enable fuzzy searching + export MCFLY_FUZZY=2 + + # Change the maximum number of results shown (default: 10) + export MCFLY_RESULTS=60 + + # To swap the color scheme for use in a light terminal, change this + export MCFLY_LIGHT=FALSE + +# Rich Enhanced Shell History (resh) Context-based replacement/enhancement for zsh and bash shell history +# (shows more information, has raw mode, but uses own database, does not have favorites, can't delete history) +# Link: https://github.com/curusarn/resh +# Install: (git clone https://github.com/curusarn/resh.git && cd resh && scripts/rawinstall.sh) +# Update: reshctl update +# WARNING: Install automatically adds lines to the end of the ~/.bashrc file +elif [[ -f ~/.resh/shellrc ]]; then + + # Source the scripts + builtin source ~/.resh/shellrc + [[ -f ~/.bash-preexec.sh ]] && builtin source ~/.bash-preexec.sh + + # Bind 'kill last command' to CTRL+x k + bind '"\C-xk": "\C-a hstr -k \C-j"' + + # Alias hh for resh + alias hh='resh' + +fi + +####################################################### +# qfc Command Line File Completion (CTRL+F to list files) +# Link: https://github.com/pindexis/qfc +# Install: git clone https://github.com/pindexis/qfc ${HOME}/.qfc +####################################################### + +# If qfc is installed, run it's initiation script +# CTRL+f will pop up to select directories or files +# CTRL+/ to cd into directory using qfc +if [[ -f "${HOME}/.qfc/bin/qfc.sh" ]]; then + builtin source "${HOME}/.qfc/bin/qfc.sh" + qfc_quick_command 'cd' '\C-_' 'cd "$0"' + qfc_quick_command 'edit' '\C-e' 'edit $0' +elif [[ -f "${BASHRC_INSTALL_DIR}/.qfc/bin/qfc.sh" ]]; then + builtin source "${BASHRC_INSTALL_DIR}/.qfc/bin/qfc.sh" + qfc_quick_command 'cd' '\C-_' 'cd "$0"' + qfc_quick_command 'edit' '\C-e' 'edit $0' +elif [[ -f /usr/share/qfc/qfc.sh ]]; then + builtin source /usr/share/qfc/qfc.sh + qfc_quick_command 'cd' '\C-_' 'cd "$0"' + qfc_quick_command 'edit' '\C-e' 'edit $0' +fi + +####################################################### +# Settings and Exports +####################################################### + +# Make sure 256 color terminals are enabled +# export TERM=xterm-256color +# Linux tries very hard to set it to a sane value depending on things +# like which terminal you are actually using and how you are connected +# You can override a particular value which the login process +# often chooses but which is not to your liking +case $TERM in "") TERM=xterm-256color;; esac + +# For use with LS_COLORS +export use_color=true + +# Tell NCURSES to use UTF-8 encoding +export NCURSES_NO_UTF8_ACS=1 + +if hascommand --strict moar; then + # Use moar + # Link: https://github.com/walles/moar + export PAGER='moar' + export MANPAGER='moar' + alias less='moar' + alias les='moar -no-linenumbers' +elif [[ -n "$LESSOPEN" ]]; then + # Use less over most if it has syntax highlighting + export PAGER='less' + export MANPAGER='less' + alias les='less -n' +elif hascommand --strict most; then + # Use most + # Link: https://www.jedsoft.org/most/ + export PAGER='most' + export MANPAGER='most' + alias less='most' + alias les='command less -n' +else + export PAGER='less' + export MANPAGER='less' + alias les='less -n' +fi + +# If bat is installed... +# https://github.com/sharkdp/bat +if hascommand --strict batcat; then + export PAGER='less' # Bat expects and uses less + export MANPAGER='batcat --style=plain' + alias bat='batcat --force-colorization' +elif hascommand --strict bat; then + export PAGER='less' # Bat expects and uses less + export MANPAGER='bat --style=plain' + alias bat='bat --force-colorization' +fi + +# If bat-extras is installed with the extra commands: +# batgrep, batman, batpipe, batwatch, batdiff, prettybat +# Link: https://github.com/eth-p/bat-extras +if hascommand --strict batman; then + alias man='batman' +fi + +# ccat is the colorizing cat +# Link: https://github.com/owenthereal/ccat +if hascommand --strict ccat; then + alias cat='ccat' +fi + +# Color for manpages in less makes manpages a little easier to read +if [[ -f /usr/share/source-highlight/src-hilite-lesspipe.sh ]]; then + export LESSOPEN="| /usr/share/source-highlight/src-hilite-lesspipe.sh %s" +elif hascommand --strict src-hilite-lesspipe.sh; then + export LESSOPEN="| src-hilite-lesspipe.sh %s" +fi + +# NOTE: Use \less with the back-slash to remove line numbers +# or you can use -n or --line-numbers if moar/most are not installed +export LESS='-N -x4 --force --ignore-case --quit-if-one-screen --no-init --RAW-CONTROL-CHARS --LONG-PROMPT --prompt=%t?f%f :stdin .?pb%pb\%:?lbLine %lb:?bbByte %bb:-...' +export LESS_TERMCAP_mb=$'\E[01;31m' +export LESS_TERMCAP_md=$'\E[01;31m' +export LESS_TERMCAP_me=$'\E[0m' +export LESS_TERMCAP_se=$'\E[0m' +export LESS_TERMCAP_so=$'\E[01;44;33m' +export LESS_TERMCAP_ue=$'\E[0m' +export LESS_TERMCAP_us=$'\E[01;32m' + +# Make less more friendly for non-text input files, see lesspipe(1) +[ -x /usr/bin/lesspipe ] && eval "$(SHELL=/bin/sh lesspipe)" + +####################################################### +# Vivid LS_COLORS Generator +# Link: https://github.com/sharkdp/vivid +# Themes: https://github.com/sharkdp/vivid/tree/master/themes +####################################################### +# LS_COLORS (lscolors-git in Arch AUR repository) +# Link: https://github.com/trapd00r/LS_COLORS +# Install: +# mkdir /tmp/LS_COLORS && curl -L https://api.github.com/repos/trapd00r/LS_COLORS/tarball/master | tar xzf - --directory=/tmp/LS_COLORS --strip=1 +# ( cd /tmp/LS_COLORS && sh install.sh ) +####################################################### + +# Colors for ls +export CLICOLOR=1 + +if hascommand --strict vivid; then + # Vivid is installed + export LS_COLORS="$(vivid generate snazzy)" + _LS_COLORS_SOURCE="Vivid" +elif [[ -f "${XDG_DATA_HOME:-${HOME}/.local/share}/lscolors.sh" ]]; then + # LS_COLORS is installed locally + builtin source "${XDG_DATA_HOME:-${HOME}/.local/share}/lscolors.sh" + _LS_COLORS_SOURCE="LS_COLORS Local" +elif [[ -f /usr/share/LS_COLORS/dircolors.sh ]]; then + # LS_COLORS is installed system wide + builtin source /usr/share/LS_COLORS/dircolors.sh + _LS_COLORS_SOURCE="LS_COLORS System Wide" +elif hascommand --strict dircolors; then + # dircolors is used by ls to set LS_COLORS for colorized directory output + # Check if a custom .dircolors file exists in the user's home directory + if test -r ~/.dircolors; then + # If the file exists and is readable, use it to set LS_COLORS instead + eval "$(dircolors -b ~/.dircolors)" + else + # If no custom .dircolors file exists then use the default settings + eval "$(dircolors -b)" + fi + _LS_COLORS_SOURCE="dircolors" +else + # Otherwise, export a full featured custom ls color profile + LS_COLORS='bd=38;5;68:ca=38;5;17:cd=38;5;113;1:di=38;5;30:do=38;5;127:ex=38;5;208;1:pi=38;5;126:fi=0:ln=target:mh=38;5;222;1:no=0:or=48;5;196;38;5;232;1:ow=38;5;220;1:sg=48;5;3;38;5;0:su=38;5;220;1;3;100;1:so=38;5;197:st=38;5;86;48;5;234:tw=48;5;235;38;5;139;3:*LS_COLORS=48;5;89;38;5;197;1;3;4;7:*.txt=38;5;253:*README=38;5;220;1:*README.rst=38;5;220;1:*README.md=38;5;220;1:*LICENSE=38;5;220;1:*COPYING=38;5;220;1:*INSTALL=38;5;220;1:*COPYRIGHT=38;5;220;1:*AUTHORS=38;5;220;1:*HISTORY=38;5;220;1:*CONTRIBUTORS=38;5;220;1:*PATENTS=38;5;220;1:*VERSION=38;5;220;1:*NOTICE=38;5;220;1:*CHANGES=38;5;220;1:*.log=38;5;190:*.adoc=38;5;184:*.asciidoc=38;5;184:*.etx=38;5;184:*.info=38;5;184:*.markdown=38;5;184:*.md=38;5;184:*.mkd=38;5;184:*.nfo=38;5;184:*.org=38;5;184:*.pod=38;5;184:*.rst=38;5;184:*.tex=38;5;184:*.textile=38;5;184:*.bib=38;5;178:*.json=38;5;178:*.jsonl=38;5;178:*.jsonnet=38;5;178:*.libsonnet=38;5;142:*.ndjson=38;5;178:*.msg=38;5;178:*.pgn=38;5;178:*.rss=38;5;178:*.xml=38;5;178:*.fxml=38;5;178:*.toml=38;5;178:*.yaml=38;5;178:*.yml=38;5;178:*.RData=38;5;178:*.rdata=38;5;178:*.xsd=38;5;178:*.dtd=38;5;178:*.sgml=38;5;178:*.rng=38;5;178:*.rnc=38;5;178:*.accdb=38;5;60:*.accde=38;5;60:*.accdr=38;5;60:*.accdt=38;5;60:*.db=38;5;60:*.fmp12=38;5;60:*.fp7=38;5;60:*.localstorage=38;5;60:*.mdb=38;5;60:*.mde=38;5;60:*.sqlite=38;5;60:*.typelib=38;5;60:*.nc=38;5;60:*.cbr=38;5;141:*.cbz=38;5;141:*.chm=38;5;141:*.djvu=38;5;141:*.pdf=38;5;141:*.PDF=38;5;141:*.mobi=38;5;141:*.epub=38;5;141:*.docm=38;5;111;4:*.doc=38;5;111:*.docx=38;5;111:*.odb=38;5;111:*.odt=38;5;111:*.rtf=38;5;111:*.pages=38;5;111:*.odp=38;5;166:*.pps=38;5;166:*.ppt=38;5;166:*.pptx=38;5;166:*.ppts=38;5;166:*.pptxm=38;5;166;4:*.pptsm=38;5;166;4:*.csv=38;5;78:*.tsv=38;5;78:*.numbers=38;5;112:*.ods=38;5;112:*.xla=38;5;76:*.xls=38;5;112:*.xlsx=38;5;112:*.xlsxm=38;5;112;4:*.xltm=38;5;73;4:*.xltx=38;5;73:*.key=38;5;166:*config=1:*cfg=1:*conf=1:*rc=1:*authorized_keys=1:*known_hosts=1:*.ini=1:*.plist=1:*.profile=1:*.bash_profile=1:*.bash_login=1:*.bash_logout=1:*.zshenv=1:*.zprofile=1:*.zlogin=1:*.zlogout=1:*.viminfo=1:*.pcf=1:*.psf=1:*.hidden-color-scheme=1:*.hidden-tmTheme=1:*.last-run=1:*.merged-ca-bundle=1:*.sublime-build=1:*.sublime-commands=1:*.sublime-keymap=1:*.sublime-settings=1:*.sublime-snippet=1:*.sublime-project=1:*.sublime-workspace=1:*.tmTheme=1:*.user-ca-bundle=1:*.rstheme=1:*.epf=1:*.git=38;5;197:*.gitignore=38;5;240:*.gitattributes=38;5;240:*.gitmodules=38;5;240:*.awk=38;5;172:*.bash=38;5;172:*.bat=38;5;172:*.BAT=38;5;172:*.sed=38;5;172:*.sh=38;5;172:*.zsh=38;5;172:*.vim=38;5;172:*.kak=38;5;172:*.ahk=38;5;41:*.py=38;5;41:*.ipynb=38;5;41:*.xsh=38;5;41:*.rb=38;5;41:*.gemspec=38;5;41:*.pl=38;5;208:*.PL=38;5;160:*.pm=38;5;203:*.t=38;5;114:*.msql=38;5;222:*.mysql=38;5;222:*.pgsql=38;5;222:*.sql=38;5;222:*.tcl=38;5;64;1:*.r=38;5;49:*.R=38;5;49:*.gs=38;5;81:*.clj=38;5;41:*.cljs=38;5;41:*.cljc=38;5;41:*.cljw=38;5;41:*.scala=38;5;41:*.sc=38;5;41:*.dart=38;5;51:*.asm=38;5;81:*.cl=38;5;81:*.lisp=38;5;81:*.rkt=38;5;81:*.el=38;5;81:*.elc=38;5;241:*.eln=38;5;241:*.lua=38;5;81:*.moon=38;5;81:*.c=38;5;81:*.C=38;5;81:*.h=38;5;110:*.H=38;5;110:*.tcc=38;5;110:*.c++=38;5;81:*.h++=38;5;110:*.hpp=38;5;110:*.hx=38;5;110:*.hxx=38;5;110:*.hxsl=38;5;110:*.ii=38;5;110:*.M=38;5;110:*.m=38;5;110:*.cc=38;5;81:*.cs=38;5;81:*.cp=38;5;81:*.cpp=38;5;81:*.cxx=38;5;81:*.cr=38;5;81:*.go=38;5;81:*.f=38;5;81:*.F=38;5;81:*.for=38;5;81:*.ftn=38;5;81:*.f90=38;5;81:*.F90=38;5;81:*.f95=38;5;81:*.F95=38;5;81:*.f03=38;5;81:*.F03=38;5;81:*.f08=38;5;81:*.F08=38;5;81:*.nim=38;5;81:*.nimble=38;5;81:*.s=38;5;110:*.S=38;5;110:*.rs=38;5;81:*.scpt=38;5;219:*.swift=38;5;219:*.sx=38;5;81:*.vala=38;5;81:*.vapi=38;5;81:*.hi=38;5;110:*.hs=38;5;81:*.lhs=38;5;81:*.agda=38;5;81:*.lagda=38;5;81:*.lagda.tex=38;5;81:*.lagda.rst=38;5;81:*.lagda.md=38;5;81:*.agdai=38;5;110:*.zig=38;5;81:*.v=38;5;81:*.pyc=38;5;240:*.tf=38;5;168:*.tfstate=38;5;168:*.tfvars=38;5;168:*.css=38;5;125;1:*.less=38;5;125;1:*.sass=38;5;125;1:*.scss=38;5;125;1:*.htm=38;5;125;1:*.html=38;5;125;1:*.jhtm=38;5;125;1:*.mht=38;5;125;1:*.eml=38;5;125;1:*.mustache=38;5;125;1:*.coffee=38;5;074;1:*.java=38;5;074;1:*.js=38;5;074;1:*.mjs=38;5;074;1:*.jsm=38;5;074;1:*.jsp=38;5;074;1:*.ada=38;5;81:*.cbl=38;5;81:*.conf=38;5;81:*.cpy=38;5;81:*.ctp=38;5;81:*.erl=38;5;81:*.groovy=38;5;81:*.hrl=38;5;81:*.inc=38;5;81:*.ino=38;5;81:*.kt=38;5;81:*.lib=38;5;81:*.mat=38;5;81:*.mk=38;5;81:*.pascal=38;5;81:*.php=38;5;81:*.plx=38;5;81:*.sml=38;5;81:*.template=38;5;81:*.tpl=38;5;81:*.twig=38;5;81:*.vb=38;5;81:*.vba=38;5;81:*.vbs=38;5;81:*.wren=38;5;81:*Dockerfile=38;5;155:*.dockerignore=38;5;240:*Makefile=38;5;155:*MANIFEST=38;5;243:*pm_to_blib=38;5;240:*.nix=38;5;155:*.dhall=38;5;178:*.rake=38;5;155:*.am=38;5;242:*.in=38;5;242:*.hin=38;5;242:*.scan=38;5;242:*.m4=38;5;242:*.old=38;5;242:*.out=38;5;242:*.SKIP=38;5;244:*.diff=48;5;197;38;5;232:*.patch=48;5;197;38;5;232;1:*.bmp=38;5;97:*.dicom=38;5;97:*.tiff=38;5;97:*.tif=38;5;97:*.TIFF=38;5;97:*.cdr=38;5;97:*.flif=38;5;97:*.gif=38;5;97:*.icns=38;5;97:*.ico=38;5;97:*.jpeg=38;5;97:*.JPG=38;5;97:*.jpg=38;5;97:*.nth=38;5;97:*.png=38;5;97:*.psd=38;5;97:*.pxd=38;5;97:*.pxm=38;5;97:*.xpm=38;5;97:*.webp=38;5;97:*.ai=38;5;99:*.eps=38;5;99:*.epsf=38;5;99:*.drw=38;5;99:*.ps=38;5;99:*.svg=38;5;99:*.avi=38;5;114:*.divx=38;5;114:*.IFO=38;5;114:*.m2v=38;5;114:*.m4v=38;5;114:*.mkv=38;5;114:*.MOV=38;5;114:*.mov=38;5;114:*.mp4=38;5;114:*.mpeg=38;5;114:*.mpg=38;5;114:*.ogm=38;5;114:*.rmvb=38;5;114:*.sample=38;5;114:*.wmv=38;5;114:*.3g2=38;5;115:*.3gp=38;5;115:*.gp3=38;5;115:*.webm=38;5;115:*.gp4=38;5;115:*.asf=38;5;115:*.flv=38;5;115:*.ts=38;5;115:*.ogv=38;5;115:*.f4v=38;5;115:*.VOB=38;5;115;1:*.vob=38;5;115;1:*.ass=38;5;117:*.srt=38;5;117:*.ssa=38;5;117:*.sub=38;5;117:*.sup=38;5;117:*.vtt=38;5;117:*.3ga=38;5;137;1:*.S3M=38;5;137;1:*.aac=38;5;137;1:*.amr=38;5;137;1:*.au=38;5;137;1:*.caf=38;5;137;1:*.dat=38;5;137;1:*.dts=38;5;137;1:*.fcm=38;5;137;1:*.m4a=38;5;137;1:*.mod=38;5;137;1:*.mp3=38;5;137;1:*.mp4a=38;5;137;1:*.oga=38;5;137;1:*.ogg=38;5;137;1:*.opus=38;5;137;1:*.s3m=38;5;137;1:*.sid=38;5;137;1:*.wma=38;5;137;1:*.ape=38;5;136;1:*.aiff=38;5;136;1:*.cda=38;5;136;1:*.flac=38;5;136;1:*.alac=38;5;136;1:*.mid=38;5;136;1:*.midi=38;5;136;1:*.pcm=38;5;136;1:*.wav=38;5;136;1:*.wv=38;5;136;1:*.wvc=38;5;136;1:*.afm=38;5;66:*.fon=38;5;66:*.fnt=38;5;66:*.pfb=38;5;66:*.pfm=38;5;66:*.ttf=38;5;66:*.otf=38;5;66:*.woff=38;5;66:*.woff2=38;5;66:*.PFA=38;5;66:*.pfa=38;5;66:*.7z=38;5;40:*.a=38;5;40:*.arj=38;5;40:*.bz2=38;5;40:*.cpio=38;5;40:*.gz=38;5;40:*.lrz=38;5;40:*.lz=38;5;40:*.lzma=38;5;40:*.lzo=38;5;40:*.rar=38;5;40:*.s7z=38;5;40:*.sz=38;5;40:*.tar=38;5;40:*.tbz=38;5;40:*.tgz=38;5;40:*.warc=38;5;40:*.WARC=38;5;40:*.xz=38;5;40:*.z=38;5;40:*.zip=38;5;40:*.zipx=38;5;40:*.zoo=38;5;40:*.zpaq=38;5;40:*.zst=38;5;40:*.zstd=38;5;40:*.zz=38;5;40:*.apk=38;5;215:*.ipa=38;5;215:*.deb=38;5;215:*.rpm=38;5;215:*.jad=38;5;215:*.jar=38;5;215:*.ear=38;5;215:*.war=38;5;215:*.cab=38;5;215:*.pak=38;5;215:*.pk3=38;5;215:*.vdf=38;5;215:*.vpk=38;5;215:*.bsp=38;5;215:*.dmg=38;5;215:*.crx=38;5;215:*.xpi=38;5;215:*.iso=38;5;124:*.img=38;5;124:*.bin=38;5;124:*.nrg=38;5;124:*.qcow=38;5;124:*.fvd=38;5;124:*.sparseimage=38;5;124:*.toast=38;5;124:*.vcd=38;5;124:*.vdi=38;5;124:*.vhd=38;5;124:*.vhdl=38;5;124:*.vhdx=38;5;124:*.vfd=38;5;124:*.vmdk=38;5;124:*.swp=38;5;244:*.swo=38;5;244:*.tmp=38;5;244:*.sassc=38;5;244:*.pacnew=38;5;33:*.un~=38;5;241:*.orig=38;5;241:*.BUP=38;5;241:*.bak=38;5;241:*.o=38;5;241:*core=38;5;241:*.mdump=38;5;241:*.rlib=38;5;241:*.dll=38;5;241:*.aria2=38;5;241:*.dump=38;5;241:*.stackdump=38;5;241:*.zcompdump=38;5;241:*.zwc=38;5;241:*.part=38;5;239:*.r[0-9]{0,2}=38;5;239:*.zx[0-9]{0,2}=38;5;239:*.z[0-9]{0,2}=38;5;239:*.pid=38;5;248:*.state=38;5;248:*lockfile=38;5;248:*lock=38;5;248:*.err=38;5;160;1:*.error=38;5;160;1:*.stderr=38;5;160;1:*.pcap=38;5;29:*.cap=38;5;29:*.dmp=38;5;29:*.allow=38;5;112:*.deny=38;5;196:*.service=38;5;45:*@.service=38;5;45:*.socket=38;5;45:*.swap=38;5;45:*.device=38;5;45:*.mount=38;5;45:*.automount=38;5;45:*.target=38;5;45:*.path=38;5;45:*.timer=38;5;45:*.snapshot=38;5;45:*.lnk=38;5;39:*.application=38;5;116:*.cue=38;5;116:*.description=38;5;116:*.directory=38;5;116:*.m3u=38;5;116:*.m3u8=38;5;116:*.md5=38;5;116:*.properties=38;5;116:*.sfv=38;5;116:*.theme=38;5;116:*.torrent=38;5;116:*.urlview=38;5;116:*.webloc=38;5;116:*.asc=38;5;192;3:*.bfe=38;5;192;3:*.enc=38;5;192;3:*.gpg=38;5;192;3:*.signature=38;5;192;3:*.sig=38;5;192;3:*.p12=38;5;192;3:*.pem=38;5;192;3:*.pgp=38;5;192;3:*.p7s=38;5;192;3:*id_dsa=38;5;192;3:*id_rsa=38;5;192;3:*id_ecdsa=38;5;192;3:*id_ed25519=38;5;192;3:*.32x=38;5;213:*.cdi=38;5;213:*.fm2=38;5;213:*.rom=38;5;213:*.sav=38;5;213:*.st=38;5;213:*.a00=38;5;213:*.a52=38;5;213:*.A64=38;5;213:*.a64=38;5;213:*.a78=38;5;213:*.adf=38;5;213:*.atr=38;5;213:*.gb=38;5;213:*.gba=38;5;213:*.gbc=38;5;213:*.gel=38;5;213:*.gg=38;5;213:*.ggl=38;5;213:*.ipk=38;5;213:*.j64=38;5;213:*.nds=38;5;213:*.nes=38;5;213:*.sms=38;5;213:*.8xp=38;5;121:*.8eu=38;5;121:*.82p=38;5;121:*.83p=38;5;121:*.8xe=38;5;121:*.stl=38;5;216:*.dwg=38;5;216:*.ply=38;5;216:*.wrl=38;5;216:*.xib=38;5;208:*.iml=38;5;166:*.DS_Store=38;5;239:*.localized=38;5;239:*.CFUserTextEncoding=38;5;239:*CodeResources=38;5;239:*PkgInfo=38;5;239:*.nib=38;5;57:*.car=38;5;57:*.dylib=38;5;241:*.entitlements=1:*.pbxproj=1:*.strings=1:*.storyboard=38;5;196:*.xcconfig=1:*.xcsettings=1:*.xcuserstate=1:*.xcworkspacedata=1:*.pot=38;5;7:*.pcb=38;5;7:*.mm=38;5;7:*.gbr=38;5;7:*.scm=38;5;7:*.xcf=38;5;7:*.spl=38;5;7:*.Rproj=38;5;11:*.sis=38;5;7:*.1p=38;5;7:*.3p=38;5;7:*.cnc=38;5;7:*.def=38;5;7:*.ex=38;5;7:*.example=38;5;7:*.feature=38;5;7:*.ger=38;5;7:*.ics=38;5;7:*.map=38;5;7:*.mf=38;5;7:*.mfasl=38;5;7:*.mi=38;5;7:*.mtx=38;5;7:*.pc=38;5;7:*.pi=38;5;7:*.plt=38;5;7:*.rdf=38;5;7:*.ru=38;5;7:*.sch=38;5;7:*.sty=38;5;7:*.sug=38;5;7:*.tdy=38;5;7:*.tfm=38;5;7:*.tfnt=38;5;7:*.tg=38;5;7:*.vcard=38;5;7:*.vcf=38;5;7:*.xln=38;5;7:' + _LS_COLORS_SOURCE="Custom Colors" +fi + +####################################################### +# grc Generic Colouriser +# Link: https://github.com/garabik/grc +####################################################### + +if [[ "${_SKIP_GRC}" = false ]] && hascommand --strict grc; then + GRC_ALIASES=true + + # If we are not defaulting to manually set values and using includes... + if [[ "${_GRC_USE_BASHRC_BUILTIN}" != true ]]; then + # Source the first include found + if [[ -f "${HOME}/.local/bin/grc.sh" ]]; then + builtin source "${HOME}/.local/bin/grc.sh" + elif [[ -f "/etc/profile.d/grc.sh" ]]; then + builtin source "/etc/profile.d/grc.sh" + elif [[ -f "/etc/grc.sh" ]]; then + builtin source "/etc/grc.sh" + else + # No includes found, we will have to use the built-in aliases + _GRC_USE_BASHRC_BUILTIN=true + fi + fi + + # If we need to use the manual built-in aliases... + if [[ "${_GRC_USE_BASHRC_BUILTIN}" == true ]]; then + # Find the grc command + GRC="$(command -v grc)" + + # If the terminal is interactive and GRC command is found... + if tty -s && [ -n "${TERM}" ] && [ "${TERM}" != "dumb" ] && [ -n "${GRC}" ]; then + alias colourify="${GRC} -es" + alias as='colourify as' + alias blkid='colourify blkid' + alias configure='colourify ./configure' + [[ "$(type -t df)" != 'alias' ]] && alias df='colourify df --human-readable --print-type --exclude-type=squashfs' + [[ "$(type -t diff)" != 'alias' ]] && alias diff='colourify diff' + alias dig='colourify dig' + alias docker='colourify docker' + alias docker-compose='colourify docker-compose' + alias docker-machine='colourify docker-machine' + alias du='colourify du' + # alias env='colourify env' + alias fdisk='colourify fdisk' + alias findmnt='colourify findmnt' + alias free='colourify free -m' + alias g++='colourify g++' + alias gas='colourify gas' + alias gcc='colourify gcc' + alias getsebool='colourify getsebool' + alias head='colourify head' + alias id='colourify id' + alias ifconfig='colourify ifconfig' + alias ip='colourify ip' + alias iptables='colourify iptables' + alias journalctl='colourify journalctl' + alias kubectl='colourify kubectl' + alias ld='colourify ld' + alias lsof='colourify lsof' + alias lspci='colourify lspci' + alias m='colourify mount' + alias make='colourify make' + alias mount='colourify mount' + alias mtr='colourify mtr' + alias netstat='colourify netstat' + alias ping='colourify ping -c 5' + # alias ps='colourify ps auxf' + alias semanage='colourify semanage' + alias sockstat='colourify sockstat' + alias ss='colourify ss' + alias tail='colourify tail' + alias traceroute='colourify traceroute' + alias traceroute6='colourify traceroute6' + fi + fi + + # Aliasing ps causes issues with some scripts + alias ps &>/dev/null && unalias ps + + # Create another alias for ps color instead + alias pss='colourify ps auxf' + + # Override lsblk to enhance the color output + #alias lsblk='colourify lsblk --exclude 1,7 --output NAME,MAJ:MIN,RM,SIZE,RO,TYPE,MOUNTPOINTS' + alias lsblk='colourify lsblk --exclude 1,7 --output NAME,MAJ:MIN,TYPE,FSTYPE,RM,MOUNTPOINTS,LABEL,SIZE,FSUSE%,RO,UUID 2> /dev/null || colourify lsblk --exclude 1,7' + +else # grc is not installed... + # List block devices but show more info including the files systems and permissions + # and removes all mem and loopback devices (like snap packages) from the list + #alias lsblk='command lsblk --exclude 1,7 --output NAME,MAJ:MIN,RM,SIZE,RO,TYPE,MOUNTPOINTS' + alias lsblk='command lsblk --exclude 1,7 --output NAME,MAJ:MIN,TYPE,FSTYPE,RM,MOUNTPOINTS,LABEL,SIZE,FSUSE%,RO,UUID 2> /dev/null || command lsblk --exclude 1,7' + + # Make df look better if not already aliased at this point + [[ "$(type -t df)" != 'alias' ]] && alias df='command df --human-readable --print-type --exclude-type=squashfs' +fi + +####################################################### +# mysql-colorize +# Link: https://github.com/zpm-zsh/mysql-colorize +# Install: git clone https://github.com/horosgrisa/mysql-colorize.bash ~/.bash/mysql-colorize +####################################################### + +# If qfc is installed +if [[ -f "${HOME}/.bash/mysql-colorize/mysql-colorize.bash" ]]; then + builtin source "${HOME}/.bash/mysql-colorize/mysql-colorize.bash" +fi + +####################################################### +# Nethogs shows which processes are using network bandwidth +# Link: https://github.com/raboof/nethogs +####################################################### +# lsof command stands for List Of Open File +# Link: https://www.geeksforgeeks.org/lsof-command-in-linux-with-examples/ +####################################################### +# iftop is a network analyzing tool used to view the bandwidth related stats +# Link: https://www.geeksforgeeks.org/iftop-command-in-linux-with-examples/ +####################################################### + +if hascommand --strict nethogs; then + # Watch real time network activity by process + alias netwatch='sudo nethogs -d 0.5 -C' +elif hascommand --strict iftop; then + # Use iftop + alias netwatch='sudo iftop' +else + # Watch real time network activity using lsof instead + alias netwatch='lsof -i -r 10' +fi + +####################################################### +# Diff Enhancement +####################################################### + +if hascommand --strict delta; then + if hascommand --strict batdiff; then + # bat-extras diff uses delta + # Link: https://github.com/eth-p/bat-extras + alias diff='batdiff --delta' + else + # delta - Beautiful side by side colored diff with Git support and syntax highlighting + # Link: https://github.com/dandavison/delta + # Info: Add listed settings to your ~/.gitconfig + alias diff='delta --side-by-side --line-numbers' + fi + export DIFFPROG='delta --side-by-side --line-numbers' +elif hascommand --strict difft; then + # Difftastic is a structural diff tool that compares files based on their syntax + # Supports over 30 programming languages + # Link: https://github.com/Wilfred/difftastic + alias diff='difft' + export DIFFPROG='difft' +elif hascommand --strict icdiff; then + # Icdiff - Improved (side by side) colored diff + # Link: https://github.com/jeffkaufman/icdiff + alias diff='icdiff --line-numbers --strip-trailing-cr' + export DIFFPROG="icdiff --line-numbers --strip-trailing-cr" +elif hascommand --strict diff-so-fancy; then + # diff-so-fancy strives to make your diffs human readable instead of machine readable + # Link: https://github.com/so-fancy/diff-so-fancy + alias diff='_diff_f() { command diff -u "$@" | diff-so-fancy; }; _diff_f' + export DIFFPROG='diff-so-fancy' +elif hascommand --strict colordiff; then + # Colorize diff output if colordiff is installed + alias diff='colordiff' + export DIFFPROG="colordiff" +else + if [[ ${EDITOR} = 'nvim' ]]; then + export DIFFPROG="nvim -d" + elif [[ ${EDITOR} = 'vim' ]]; then + export DIFFPROG="vim -d" + else + export DIFFPROG="command diff --side-by-side --suppress-common-lines --ignore-all-space --ignore-blank-lines --strip-trailing-cr --report-identical-files" + fi +fi + +####################################################### +# Desktop Environment +####################################################### + +# If we are inside a desktop environment (and not TTY or SSH) +if ([[ -n "$DISPLAY" ]] || [[ -n "$WAYLAND_DISPLAY" ]]) && [[ -n "$XDG_CURRENT_DESKTOP" ]]; then + + # Alias to log out the currect user + alias logout="sudo pkill -u ${USER}" + + # Check if the user's session type is X11 + if [[ "${XDG_SESSION_TYPE}" == "x11" ]]; then + # Ensure the ffmpeg command is available + if hascommand --strict ffmpeg; then + # Create an alias to capture video on X11 using ffmpeg + # Documentation for ffmpeg: https://ffmpeg.org/documentation.html + alias grabvideo='ffmpeg -f x11grab -s wxga -r 25 -i :0.0 -qscale 0' + fi + + # Fix QT_QPA_PLATFORM if wrongly set to offscreen (makes Qt/KDE apps invisible) + if [[ $QT_QPA_PLATFORM == "offscreen" ]]; then + export QT_QPA_PLATFORM=xcb + fi + + # Check if the user's session type is Wayland + elif [[ "${XDG_SESSION_TYPE}" == "wayland" ]]; then + # Ensure the wf-recorder command is available (requires wl-roots compositor) + if hascommand wf-recorder; then + # Create an alias to capture video on Wayland using wf-recorder + # Documentation for wf-recorder: https://github.com/ammen99/wf-recorder + alias grabvideo='wf-recorder -f output.mp4' + fi + + # Fix QT_QPA_PLATFORM if wrongly set to offscreen (makes Qt/KDE apps invisible) + if [[ $QT_QPA_PLATFORM == "offscreen" ]]; then + export QT_QPA_PLATFORM=wayland + fi + fi + + # If a gui diff/merge application is installed, use that instead + for _DIFF_APP_GUI in \ + meld \ + kompare \ + kdiff3 \ + xxdiff + do + if hascommand --strict ${_DIFF_APP_GUI}; then + # Create a function capturing the diff tool value at define time + # (eval-based closure so the function doesn't depend on the loop variable) + eval "gdiff() { \"${_DIFF_APP_GUI}\" \"\$@\" > /dev/null 2>&1 & disown; }" + + # Optionally, set DIFFPROG + # export DIFFPROG="${_DIFF_APP_GUI}" + break + fi + done + + # Loop through potential file managers and set alias for the first found + for _FILE_MANAGER in \ + kde-open \ + gnome-open \ + xdg-open \ + exo-open \ + krusader \ + doublecmd \ + dolphin \ + thunar \ + pcmanfm \ + nautilus \ + nemo \ + caja \ + konqueror \ + ranger \ + nnn \ + mc + do + if hascommand --strict ${_FILE_MANAGER}; then + if [[ "${_FILE_MANAGER}" == "kde-open" ]]; then + # Only use kde-open on KDE desktops + [[ "$XDG_CURRENT_DESKTOP" == *KDE* ]] || continue + alias ui="kde-open \"\${PWD}\"" + elif [[ "${_FILE_MANAGER}" == "gnome-open" ]]; then + # Only use gnome-open on GNOME desktops + [[ "$XDG_CURRENT_DESKTOP" == *GNOME* ]] || continue + alias ui="gnome-open \"\${PWD}\"" + elif [[ "${_FILE_MANAGER}" == "exo-open" ]]; then + # Only use exo-open on Xfce desktops + [[ "$XDG_CURRENT_DESKTOP" == *XFCE* ]] || continue + alias ui="exo-open --launch FileManager \"\${PWD}\" > /dev/null 2>&1 & disown" + elif [[ "${_FILE_MANAGER}" =~ ^(ranger|nnn|mc)$ ]]; then + # These commands do not need backgrounding or output suppression + alias ui="${_FILE_MANAGER} \"\${PWD}\"" + else + # General case for GUI-based file managers (xdg-open, dolphin, etc.) + alias ui="${_FILE_MANAGER} \"\${PWD}\" > /dev/null 2>&1 & disown" + fi + break # Stop the loop once the first available file manager is found + fi + done +fi + +####################################################### +# Bash Completion +# Link: https://github.com/scop/bash-completion +####################################################### + +# Use extended globbing for more advanced pattern matching +# This is necessary for programmable completion +shopt -s extglob + +if [[ ! ${_SKIP_BASH_COMPLETION} = true ]]; then + # Use bash-completion, if available + if [[ -f "/usr/share/bash-completion/bash_completion" ]]; then + builtin source "/usr/share/bash-completion/bash_completion" + elif [[ -f "/etc/bash_completion" ]]; then + builtin source "/etc/bash_completion" + elif [[ -f "${HOME}/bash_completion" ]]; then + builtin source "${HOME}/bash_completion" + elif [[ -f "${XDG_DATA_HOME:-${HOME}/.local/share}/bash_completion" ]]; then + builtin source "${XDG_DATA_HOME:-${HOME}/.local/share}/bash_completion" + elif [[ -f "${XDG_CONFIG_HOME:-${HOME}/.config}/bash_completion" ]]; then + builtin source "${XDG_CONFIG_HOME:-${HOME}/.config}/bash_completion" + fi +fi + +####################################################### +# Cod is a completion daemon for bash, fish, and zsh +# Link: https://github.com/dim-an/cod +####################################################### + +if hascommand --strict cod; then + builtin source <(cod init $$ bash) +fi + +########################################################################### +# fzf-tab-completion tab completion using fzf in bash, GNU readline apps +# Link: https://github.com/lincheney/fzf-tab-completion +# Install: git clone https://github.com/lincheney/fzf-tab-completion +########################################################################### + +# Check if the fzf-bash-completion.sh script exists in known locations +if [[ -f "/usr/share/fzf-tab-completion/bash/fzf-bash-completion.sh" ]]; then + # Source the fzf bash completion script + builtin source "/usr/share/fzf-tab-completion/bash/fzf-bash-completion.sh" + + # Bind the tab key to the fzf_bash_completion function + bind -x '"\t": fzf_bash_completion' + + # Bind Shift+Tab to the default completion function as a backup + bind '"\033[Z": complete' + + # If nodejs is installed, enable fzf-tab-completion for nodejs repl + if hascommand node; then + alias node='node -r /usr/share/fzf-tab-completion/node/fzf-node-completion.js' + fi +elif [[ -f "${HOME}/fzf-tab-completion/bash/fzf-bash-completion.sh" ]]; then + # Source the fzf bash completion script if git cloned + builtin source "${HOME}/fzf-tab-completion/bash/fzf-bash-completion.sh" + + # Bind the tab key to the fzf_bash_completion function + bind -x '"\t": fzf_bash_completion' + + # Bind Shift+Tab to the default completion function as a backup + bind '"\033[Z": complete' + + # If nodejs is installed, enable fzf-tab-completion for nodejs repl + if hascommand node; then + alias node="node -r ${HOME}/fzf-tab-completion/node/fzf-node-completion.js" + fi +elif [[ -f "${BASHRC_INSTALL_DIR}/fzf-tab-completion/bash/fzf-bash-completion.sh" ]]; then + # Source the fzf bash completion script if git cloned + builtin source "${BASHRC_INSTALL_DIR}/fzf-tab-completion/bash/fzf-bash-completion.sh" + + # Bind the tab key to the fzf_bash_completion function + bind -x '"\t": fzf_bash_completion' + + # Bind Shift+Tab to the default completion function as a backup + bind '"\033[Z": complete' + + # If nodejs is installed, enable fzf-tab-completion for nodejs repl + if hascommand node; then + alias node="node -r ${BASHRC_INSTALL_DIR}/fzf-tab-completion/node/fzf-node-completion.js" + fi +fi + +####################################################### +# Enable the "Command not found" hook +# Link: https://github.com/falconindy/pkgfile +# NOTE: pkgfile is targetted at Arch Linux users +# Install: pacman -S pkgfile +####################################################### + +if [[ -r /usr/share/doc/pkgfile/command-not-found.bash ]]; then + builtin source /usr/share/doc/pkgfile/command-not-found.bash +fi + +####################################################### +# Better Bash Defaults +# Link: http://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html +####################################################### + +# Bind the Right arrow key to forward-char (move cursor forward) +bind '"\033[C": forward-char' + +# Bind the Left arrow key to backward-char (move cursor backward) +bind '"\033[D": backward-char' + +# CTRL+u will now undo (CTRL+z may or may not work) +bind '"\C-u": undo' +# bind '"\C-z": undo' + +# CRTL+q will now clear the entire line in the terminal (remember quit command) +bind '"\C-q": kill-whole-line' + +# CTRL+d will disconnect even if there is text on the line +bind '"\C-d":"\C-k\C-u\C-d"' + +# Disable the bell sound but make it visible +bind 'set bell-style visible' + +# Update window size after each command and, if necessary, update the values of LINES and COLUMNS +# shopt -s checkwinsize +shopt -s checkwinsize + +# Automatically trim long paths in the prompt (requires Bash 4.x) +PROMPT_DIRTRIM=3 + +# Turn on recursive globbing (enables ** to recurse all directories) +# For example, ls **/*.txt will list all text files in the current directory hierarchy +shopt -s globstar 2> /dev/null + +# Case-insensitive globbing (used in pathname expansion) +shopt -s nocaseglob; + +# Report the status of terminated background jobs immediately rather than before the next primary prompt +set -o notify + +# Bash checks that a command found in the hash table exists before trying to execute it +shopt -s checkhash + +# Bash includes filenames beginning with a "." in the results of filename expansion +shopt -s dotglob + +# Disable core dumps because of the following reasons: +# 1. Security: Core dumps can contain sensitive data, such as passwords or +# encryption keys which are a potential security risk! +# 2. Disk Space: Core dumps can be large and consume significant disk space +# 3. Performance: Writing core dumps to disk can impact system performance +# particularly if crashes are frequent or if the disk is slow +# You can also edit the /etc/security/limits.conf file and add the following: +# * soft core 0 <- without the # in the beginning +# * hard core 0 <- without the # in the beginning +# For more information on disabling core dumps in systemd, visit the following: +# https://www.cyberciti.biz/faq/disable-core-dumps-in-linux-with-systemd-sysctl/ +ulimit -S -c 0 + +# Don't let my shell warn me of incoming mail +shopt -u mailwarn +unset MAILCHECK + +# Prevent overwriting an existing file with the >, >&, and <> redirection operators +# Use `>|` to force redirection to an existing file +#set -o noclobber + +# Turns off CTRL+D to log out +#set -o ignoreeof + +# Fix the HOME and END keys in PuTTY +if [[ "$COLORTERM" ]]; then # rxvt + bind '"\033[7~": beginning-of-line' + bind '"\033[8~": end-of-line' +else # xterm + bind '"\033[1~": beginning-of-line' + bind '"\033[4~": end-of-line' +fi + +####################################################### +### SMARTER TAB-COMPLETION (Readline bindings) +####################################################### + +# Perform file completion in a case insensitive fashion +bind 'set completion-ignore-case on' + +# Treat hyphens and underscores as equivalent +bind 'set completion-map-case on' + +# Display matches for ambiguous patterns at first tab press instead of bell +bind 'set show-all-if-ambiguous on' + +# This line sets the completions to be listed immediately instead of ringing +# the bell when the completing word has more than one possible completion but +# no partial completion can be made +bind 'set show-all-if-unmodified on' + +# Immediately add a trailing slash when autocompleting symlinks to directories +bind 'set mark-symlinked-directories on' + +# Add a trailing slash when completing a directory name +bind 'set mark-directories on' + +# Set autoexpansion of the '~' when TAB is pressed +bind 'set expand-tilde off' + +# This line sets readline to display possible completions using different +# colors to indicate their file types. The colors are determined by the +# environmental variable LS_COLORS, which can be nicely configured +bind 'set colored-stats on' + +# This lines sets completions to be appended by characters that indicate their +# file types reported by the stat system call +bind 'set visible-stats on' + +####################################################### +### BETTER DIRECTORY NAVIGATION +####################################################### + +# Prepend cd to directory names automatically +shopt -s autocd 2> /dev/null + +# Correct spelling errors during tab-completion +shopt -s dirspell 2> /dev/null +shopt -s direxpand 2> /dev/null + +# Correct spelling errors in arguments supplied to cd +shopt -s cdspell 2> /dev/null + +# This defines where cd looks for targets +# Add the directories you want to have fast access to, separated by colon +# Ex: CDPATH=".:~:~/projects" will look for targets in the current working directory, in home and in the ~/projects folder +CDPATH="." + +# The source builtin uses the value of PATH to find the directory containing the file supplied as an argument +shopt -s sourcepath + +# If Readline is being used, Bash will not attempt to search the PATH for possible completions when completion is attempted on an empty line +shopt -s no_empty_cmd_completion + +####################################################### +# User Specific Aliases +# This runs towards the end of the script in order to +# have supporting aliases and features and also so +# these can be over-written or modified (see unalias) +####################################################### + +if [[ -f "${HOME}/.bash_aliases" ]]; then + builtin source "${HOME}/.bash_aliases" +elif [[ -f "${BASHRC_INSTALL_DIR}/aliases" ]]; then + builtin source "${BASHRC_INSTALL_DIR}/aliases" +fi + +####################################################### +# Enhancd next-generation cd command with an interactive filter +# Link: https://github.com/b4b4r07/enhancd +# Install: cd ~ && git clone https://github.com/b4b4r07/enhancd +# NOTE: Breaks/replaces Bash shell option "shopt -s cdable_vars" +####################################################### + +# If enhancd is installed, initialize it +if [[ -f "/usr/share/enhancd/init.sh" ]]; then + ENHANCD_FILTER=fzy:sk:fzf:peco:percol:pick:icepick:selecta:sentaku:zf + export ENHANCD_FILTER + builtin source "/usr/share/enhancd/init.sh" +elif [[ -f "${HOME}/enhancd/init.sh" ]]; then + ENHANCD_FILTER=fzy:sk:fzf:peco:percol:pick:icepick:selecta:sentaku:zf + export ENHANCD_FILTER + builtin source "${HOME}/enhancd/init.sh" +elif [[ -f "${BASHRC_INSTALL_DIR}/enhancd/init.sh" ]]; then + ENHANCD_FILTER=fzy:sk:fzf:peco:percol:pick:icepick:selecta:sentaku:zf + export ENHANCD_FILTER + builtin source "${BASHRC_INSTALL_DIR}/enhancd/init.sh" +fi + +####################################################### +# Fasd keeps track of files and directories you have +# accessed so that you can quickly reference them +# a - any (directory or file) +# s - show / search / select +# d - directory +# f - file +# sd - interactive directory selection +# sf - interactive file selection +# z - cd, same functionality as j in autojump +# zz - cd with interactive selection +# Link: https://github.com/clvv/fasd +####################################################### + +# If fasd is installed, initialize it +if hascommand --strict fasd; then + eval "$(fasd --init auto)" +fi + +####################################################### +# Automatically source node.js and npm +####################################################### + +# Check if npm command exists +if hascommand --strict npm; then + + # Define an array to store possible locations of NVM initialization files + # Order of the locations matters as this will use the first file it finds + _NVM_FILE_LOCATIONS=( + "${HOME}/.nvm/init-nvm.sh" + "${HOME}/.nvm/nvm.sh" + "${XDG_DATA_HOME:-${HOME}/.local/share}/nvm/init-nvm.sh" + "${XDG_DATA_HOME:-${HOME}/.local/share}/nvm/nvm.sh" + "${XDG_CONFIG_HOME:-${HOME}/.config}/nvm/init-nvm.sh" + "${XDG_CONFIG_HOME:-${HOME}/.config}/nvm/nvm.sh" + "/usr/share/nvm/init-nvm.sh" + "/opt/nvm/init-nvm.sh" + "/opt/nvm/nvm.sh" + "/usr/share/nvm/nvm.sh" + ) + + # Iterate over the locations + for _NVM_FILE in "${_NVM_FILE_LOCATIONS[@]}"; do + # Check if the current file exists + if [[ -L "${_NVM_FILE}" || -f "${_NVM_FILE}" ]]; then + # If the file exists, source it to initialize NVM and set the + # NVM_DIR based on the directory of the found initialization file + if _RESOLVED_PATH="$(resolvesymlink "${_NVM_FILE}" 2>/dev/null)" && \ + [[ -n "${_RESOLVED_PATH}" ]] && [[ -f "${_RESOLVED_PATH}" ]] && \ + builtin source "${_RESOLVED_PATH}" 2>/dev/null && \ + NVM_DIR="$(builtin cd "$(command dirname "${_RESOLVED_PATH}")" && command pwd 2>/dev/null)"; then + + export NVM_DIR + break # Break out of the loop + fi + fi + done + + # Define an array to store possible locations of the bash_completion file + _NVM_FILE_LOCATIONS=( + "${NVM_DIR}/bash_completion" + "${HOME}/.nvm/bash_completion" + "${XDG_DATA_HOME:-${HOME}/.local/share}/nvm/bash_completion" + "${XDG_CONFIG_HOME:-${HOME}/.config}/nvm/bash_completion" + "/usr/share/nvm/bash_completion" + "/opt/nvm/bash_completion" + ) + + # Iterate over the locations + for _NVM_FILE in "${_NVM_FILE_LOCATIONS[@]}"; do + # Check if the current bash_completion file exists + if [[ -f "${_NVM_FILE}" ]]; then + # If the file exists, source it to load NVM bash completion + builtin source "${_NVM_FILE}" 2>/dev/null + break # Break out of the loop + fi + done + + # Clean up + unset _RESOLVED_PATH _NVM_FILE_LOCATIONS _NVM_FILE +fi + +####################################################### +# Automatically source all files/links in the directory: +# ~/.config/bashrc/bashrc.d +####################################################### + +# If the auto-source folder exists and has files in it... +if [[ -d "${BASHRC_INSTALL_DIR}/bashrc.d" ]]; then + + # If the directory is not empty... + if [[ "$(command ls -A "${BASHRC_INSTALL_DIR}/bashrc.d")" ]]; then + + # Loop through files (in alphabetical order) and source them + # To specify a load order, files can be prefixed with numbers + # (e.g. 00--filename, 50--filename, 95--filename) + for file in "${BASHRC_INSTALL_DIR}"/bashrc.d/*; do + builtin source "${file}" + done + fi +fi + +####################################################### +# Custom Bash Prompt Configuration +# This script configures a personalized, attractive, and informative Bash prompt +# that displays useful information such as the current user, date, time, working +# directory, and Git repository status organized by an attractive color scheme +# NOTE This default prompt will be used if another prompt is not installed +# Link: https://ezprompt.net/ (some modifications below) +####################################################### +# Git Code Reference: +# > Renamed +# * Ahead +# + New File +# ? Untracked +# x Deleted +# ! Dirty +####################################################### + +# Get current branch in Git repo +function _prompt_git_branch() { + if hascommand --strict git; then + if [[ $_GIT_IS_SLOW = false ]]; then + BRANCH=$(git branch 2> /dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/\1/') + if [[ -n "${BRANCH}" ]]; then + STAT=$(_prompt_git_status) + echo " [${BRANCH}${STAT}]" + else + echo "" + fi + else + _prompt_git_fast "${@}" + fi + else + # Git is not installed, return empty string + echo "" + fi +} + +# Get current status of Git repo +function _prompt_git_status() { + # Check if we are in a git directory + if ! git rev-parse --is-inside-work-tree > /dev/null 2>&1; then + echo "" + return + fi + + # Cache the git status + local STATUS=$(git status --porcelain 2>&1) + + # Check for conditions using pattern matching on porcelain status + # Porcelain format: XY where X=index status, Y=working-tree status + local DIRTY=$(echo "${STATUS}" | grep -c '^[ MADRCU][MD]') + local UNTRACKED=$(echo "${STATUS}" | grep -c '^??') + local AHEAD=$(git rev-list --count @{u}..HEAD 2>/dev/null || echo 0) + local NEW_FILE=$(echo "${STATUS}" | grep -c '^A') + local RENAMED=$(echo "${STATUS}" | grep -c '^R') + # Match deletions in both index (D.) and working-tree (.D) + local DELETED=$(echo "${STATUS}" | grep -c '^\(.D\|D.\)') + # Staged changes: anything with a non-space in the index column + # Exclude new/renamed/deleted since they have their own symbols + local STAGED=$(echo "${STATUS}" | grep -c '^[MC] ') + + # Build the status string + local BITS='' + [[ ${RENAMED:-0} -ne 0 ]] && BITS=">${BITS}" + [[ ${AHEAD:-0} -ne 0 ]] && BITS="*${BITS}" + [[ ${NEW_FILE:-0} -ne 0 ]] && BITS="+${BITS}" + [[ ${UNTRACKED:-0} -ne 0 ]] && BITS="?${BITS}" + [[ ${DELETED:-0} -ne 0 ]] && BITS="x${BITS}" + [[ ${DIRTY:-0} -ne 0 ]] && BITS="!${BITS}" + [[ ${STAGED:-0} -ne 0 ]] && BITS="~${BITS}" + + # Output result + [[ -n "${BITS}" ]] && echo " ${BITS}" || echo "" +} + +# Faster Git information for Git Bash and slow networks +# https://stackoverflow.com/questions/4485059/git-bash-is-extremely-slow-in-windows-7-x64/19500237#19500237 +# https://stackoverflow.com/questions/4485059/git-bash-is-extremely-slow-in-windows-7-x64/13476961#13476961 +# https://stackoverflow.com/questions/39518124/check-if-directory-is-git-repository-without-having-to-cd-into-it/39518382#39518382 +function _prompt_git_fast() { + if hascommand --strict git; then + git -C . rev-parse 2>/dev/null >/dev/null && \ + echo " [$(git symbolic-ref --short -q HEAD 2>/dev/null || \ + git rev-parse -q --short HEAD 2>/dev/null)]" + else + # Git is not installed, return empty string + echo "" + fi +} + +# Return the path for the multi-line prompt +function _prompt_pwd_full() { + # Returns the full path but still shows the home directory as ~ + echo "${PWD}" | sed "s@${HOME}@~@" +} + +# Clear out the prompt command before we begin +# (Some environments can set this and cause errors) +export PROMPT_COMMAND='' + +# Set the prompt based on the environment +if [[ "$_KERNEL_NAME" = "MINGW" ]] || [[ "$_KERNEL_NAME" = "CYGWI" ]]; then + + # If using Git Bash or Cygwin use faster and less intensive functions + export PS1='\[\033[0;35m\]\u \ +\[\033[0;34m\]\D{%b %d} \ +\[\033[0;36m\]\A \ +\[\033[0;33m\]\w\ +\[\033[0;31m\]`_prompt_git_fast`\ +\[\033[0m\]\n\$ ' + +else # Standard default prompt + + # Prompt color changes if logged on as root + if [[ ${EUID} -gt 0 ]]; then + _COLOR_USER="\033[0;32m" + else + _COLOR_USER="\033[1;31m" + fi + + # Date formats can be found here (man strftime): + # https://manpages.ubuntu.com/manpages/xenial/man3/strftime.3.html + # https://www.cyberciti.biz/faq/linux-unix-formatting-dates-for-display/ + + # Ff connected SSH, show the host... + if [[ -n "$SSH_CLIENT" ]] || [[ -n "$SSH_TTY" ]]; then + if [[ $_PROMPT_BUILTIN_FULL_PATH = false ]]; then + # Remote SSH with short path (1 line) + export PS1="\[\033[0;35m\]\u\ +@\[\033[1;35m\]\h \ +\[\033[0;34m\]\D{%b %-d} \ +\[\033[0;36m\]\D{%-H:%M} \ +\[\033[0;33m\]\w\ +\[\033[0;31m\]\$(_prompt_git_branch) \ +\[${_COLOR_USER}\]>\[\033[0m\] " + else + # Remote SSH with full path (2 lines) + export PS1="\[\033[0;35m\]\u\ +@\[\033[1;35m\]\h \ +\[\033[0;34m\]\D{%b %-d} \ +\[\033[0;36m\]\D{%-H:%M:%S} \ +\[\033[0;33m\]\$(_prompt_pwd_full)\ +\[\033[0;31m\]\$(_prompt_git_branch)\ +\[\033[0m\]\n\[${_COLOR_USER}\]>\[\033[0m\] " + fi + + else # Local prompt - only show the name + + if [[ $_PROMPT_BUILTIN_FULL_PATH = false ]]; then + # Local with short path (1 line) + export PS1="\[\033[0;35m\]\u \ +\[\033[0;34m\]\D{%b %-d} \ +\[\033[0;36m\]\D{%-H:%M} \ +\[\033[0;33m\]\w\ +\[\033[0;31m\]\$(_prompt_git_branch) \ +\[${_COLOR_USER}\]>\[\033[0m\] " + else + # Local with full path (2 lines) + export PS1="\[\033[0;35m\]\u \ +\[\033[0;34m\]\D{%b %-d} \ +\[\033[0;36m\]\D{%-H:%M:%S} \ +\[\033[0;33m\]\$(_prompt_pwd_full)\ +\[\033[0;31m\]\$(_prompt_git_branch)\ +\[\033[0m\]\n\[${_COLOR_USER}\]>\[\033[0m\] " + fi + fi +fi + +####################################################### +# Attempt to find and load some of the top installed prompts +# Only set and replace the custom prompt script if installed +# Otherwise, the standard prompt in a section above is already set +####################################################### + +# The original older Extreme Ultimate .bashrc File prompt with added Git support +if [[ -f "${HOME}/.bashrc_prompt" ]] && [[ $_SKIP_PROMPT_ORIGINAL = false ]]; then + builtin source "${HOME}/.bashrc_prompt" +elif [[ -f "${BASHRC_INSTALL_DIR}/prompt" ]] && [[ $_SKIP_PROMPT_ORIGINAL = false ]]; then + builtin source "${BASHRC_INSTALL_DIR}/prompt" + +# Trueline Bash (true 24-bit color and glyph support) +# This is the preferred prompt since it looks amazing, +# has so many features, is easily extended using functions, +# and is a single Bash script file that is easy to install. +# NOTE: You can place trueline.sh in the ~/.config/bashrc directory to keep it out of home +# Link: https://github.com/petobens/trueline +# Install: wget https://raw.githubusercontent.com/petobens/trueline/master/trueline.sh -P ~/ +# Fonts: https://github.com/powerline/fonts +elif hascommand --strict trueline && [[ $_SKIP_PROMPT_TRUELINE = false ]]; then + builtin source "$(command which trueline)" +elif [[ -f /usr/bin/trueline ]] && [[ $_SKIP_PROMPT_TRUELINE = false ]]; then + builtin source /usr/bin/trueline +elif [[ -f "${BASHRC_INSTALL_DIR}/trueline.sh" ]] && [[ $_SKIP_PROMPT_TRUELINE = false ]]; then + builtin source "${BASHRC_INSTALL_DIR}/trueline.sh" +elif [[ -f "${HOME}/trueline/trueline.sh" ]] && [[ $_SKIP_PROMPT_TRUELINE = false ]]; then + builtin source "${HOME}/trueline/trueline.sh" +elif [[ -f "${HOME}/trueline.sh" ]] && [[ $_SKIP_PROMPT_TRUELINE = false ]]; then + builtin source "${HOME}/trueline.sh" + +# Powerline-Go Global Install (this prompt uses no special glyphs) +# Link: https://github.com/justjanne/powerline-go +elif [[ -f "/usr/bin/powerline-go" ]] && [[ $_SKIP_PROMPT_POWERLINE_GO = false ]]; then + + # Prompt Configuration for Powerline-Go + function _powerline_go_update_ps1() { + PS1="$(/usr/bin/powerline-go -error $? -jobs $(jobs -p | wc -l))" + + # Automatically clear errors after showing them once (can cause problems) + #set "?" + } + PROMPT_COMMAND="_powerline_go_update_ps1; $PROMPT_COMMAND" +# Powerline-Go Home Folder Install (this prompt uses no special glyphs) +elif [[ -f "$GOPATH/bin/powerline-go" ]] && [[ $_SKIP_PROMPT_POWERLINE_GO = false ]]; then + + # Prompt Configuration for Powerline-Go + function _powerline_go_update_ps1() { + PS1="$($GOPATH/bin/powerline-go -error $? -jobs $(jobs -p | wc -l))" + + # Automatically clear errors after showing them once (can cause problems) + #set "?" + } + PROMPT_COMMAND="_powerline_go_update_ps1; $PROMPT_COMMAND" + +# Powerline-Shell (details about git/svn/hg/fossil branch and Python virtualenv environment) +# Link: https://github.com/b-ryan/powerline-shell +elif hascommand --strict powerline-shell && [[ $_SKIP_PROMPT_POWERLINE_SHELL = false ]]; then + + # Prompt Configuration for Powerline-Shell + function _powerline_shell_update_ps1() { + PS1=$(powerline-shell $?) + } + PROMPT_COMMAND="_powerline_shell_update_ps1; $PROMPT_COMMAND" + +# Pureline (256 color written in bash script) +# Link: https://github.com/chris-marsh/pureline +# Install: +# git clone https://github.com/chris-marsh/pureline.git +# cp pureline/configs/powerline_full_256col.conf ~/.pureline.conf +elif [[ -f "${HOME}/pureline/pureline" ]] && [[ $_SKIP_PROMPT_PURELINE = false ]]; then + builtin source "${HOME}/pureline/pureline" "${HOME}/.pureline.conf" + +# Starship Cross Shell Prompt (focus on compatibility and written in Rust) +# Link: https://starship.rs +# Install: sh -c "$(curl -fsSL https://starship.rs/install.sh)" +elif hascommand --strict starship && [[ $_SKIP_PROMPT_STARSHIP = false ]]; then + eval "$(starship init bash)" + +# Oh-My-Git (only used for Git but has huge support for it, requires font) +# Link: https://github.com/arialdomartini/oh-my-git +# Install: git clone https://github.com/arialdomartini/oh-my-git.git ~/.oh-my-git +elif [[ -f "${HOME}/.oh-my-git/prompt.sh" ]] && [[ $_SKIP_PROMPT_OH_MY_GIT = false ]]; then + builtin source "${HOME}/.oh-my-git/prompt.sh" + +# Bash Git Prompt (shows git repository, branch name, difference with remote branch, number of files staged, changed, etc) +# Link: https://github.com/magicmonty/bash-git-prompt +# Install: git clone https://github.com/magicmonty/bash-git-prompt.git ~/.bash-git-prompt --depth=1 +elif [[ -f /usr/lib/bash-git-prompt/gitprompt.sh ]] && [[ $_SKIP_PROMPT_BASH_GIT_PROMPT = false ]]; then + # To only show the git prompt in or under a repository directory + GIT_PROMPT_ONLY_IN_REPO=1 + # To use upstream's default theme + # GIT_PROMPT_THEME=Default + # To use upstream's default theme, modified by arch maintainer + GIT_PROMPT_THEME=Default_Arch + builtin source /usr/lib/bash-git-prompt/gitprompt.sh +elif [[ -f "${HOME}/.bash-git-prompt/gitprompt.sh" ]] && [[ $_SKIP_PROMPT_BASH_GIT_PROMPT = false ]]; then + # To only show the git prompt in or under a repository directory + GIT_PROMPT_ONLY_IN_REPO=1 + # To use upstream's default theme + # GIT_PROMPT_THEME=Default + # To use upstream's default theme, modified by arch maintainer + GIT_PROMPT_THEME=Default_Arch + builtin source "${HOME}/.bash-git-prompt/gitprompt.sh" + +# Bash Powerline (no need for patched fonts, supports git, previous command execution status, platform-dependent prompt symbols) +# Link: https://github.com/riobard/bash-powerline +# Install: curl https://raw.githubusercontent.com/riobard/bash-powerline/master/bash-powerline.sh > ~/.bash-powerline.sh +elif [[ -f "${HOME}/.bash-powerline.sh" ]] && [[ $_SKIP_PROMPT_BASH_POWERLINE = false ]]; then + builtin source "${HOME}/.bash-powerline.sh" + +# Sexy Bash Prompt (supports git, 256 color) +# Link: https://github.com/twolfson/sexy-bash-prompt +# Install: (cd /tmp && ([[ -d sexy-bash-prompt ]] || git clone --depth 1 --config core.autocrlf=false https://github.com/twolfson/sexy-bash-prompt) && cd sexy-bash-prompt && make install) +elif [[ -f "${HOME}/.bash_prompt" ]] && [[ $_SKIP_PROMPT_SEXY_BASH_PROMPT = false ]]; then + builtin source "${HOME}/.bash_prompt" + +# Liquid Prompt (adaptive prompt with low color and no glyphs) +# Link: https://github.com/nojhan/liquidprompt +# Install: git clone --branch stable https://github.com/nojhan/liquidprompt.git ~/liquidprompt +elif [[ -f "${HOME}/liquidprompt/liquidprompt" ]] && [[ $_SKIP_PROMPT_LIQUIDPROMPT = false ]]; then + builtin source "${HOME}/liquidprompt/liquidprompt" + +# Original Powerline Status Line for Vim Bash Zsh fish tmux IPython Awesome i3 Qtile +# Link: https://github.com/powerline/powerline +# Install: https://medium.com/earlybyte/powerline-for-bash-6d3dd004f6fc +# NOTE: Requires Python and can be used with Trueline in Bash +# WARNING: This path may change or break in the future with new Python versions +elif [[ $_SKIP_PROMPT_POWERLINE = false ]]; then + _POWERLINE_PATH=$(find /usr/lib/python3* -type f -path "*/site-packages/powerline/bindings/bash/powerline.sh" 2>/dev/null | head -n 1) + if [[ -f "$_POWERLINE_PATH" ]]; then + builtin source "$_POWERLINE_PATH" + fi +fi + +####################################################### +# Play nice with Midnight Commander subshell +# Link: https://midnight-commander.org/ +# Link: https://superuser.com/questions/526201/how-to-change-the-prompt-of-mcs-subshell +####################################################### + +if [[ -n "$(ps -p "${PPID}" -o comm= 2>/dev/null | grep -x mc)" ]]; then + # The Midnight Commander subshell doesn't like aliases for pwd + alias pwd &>/dev/null && unalias pwd + + # Exit here + return +fi + +####################################################### +# Blesh: Bash Line Editor replaces default GNU Readline (Do this step last) +# Link: https://github.com/akinomyoga/ble.sh +# Link for configuration: https://github.com/akinomyoga/ble.sh/blob/master/blerc +# WARNING: Can be buggy with certain prompts (like Trueline) +# To Update (in a ble.sh session): ble-update +# To Install: +# mkdir -P ~/.local/share/blesh/src && cd ~/.local/share/blesh/src +# git clone --recursive --depth 1 --shallow-submodules https://github.com/akinomyoga/ble.sh.git +# make -C ble.sh install PREFIX=~/.local +# To Run Without Installation: +# git clone --recursive --depth 1 --shallow-submodules https://github.com/akinomyoga/ble.sh.git +# make -C ble.sh +####################################################### + +# Define an array of possible locations for ble.sh (checked in order) +_BLESH_PATHS=( + "${XDG_DATA_HOME:-${HOME}/.local/share}/blesh/ble.sh" # User-level installation + "${XDG_DATA_HOME:-${HOME}/.local/share}/doc/blesh/ble.sh" # User-level installation + "${HOME}/ble.sh/out/ble.sh" # Local Git installation + "/usr/share/blesh/ble.sh" # System-wide installation +) + +# Loop through each potential path to find where ble.sh might be located +for _BLESH_PATH in "${_BLESH_PATHS[@]}"; do + # Check if ble.sh exists at the current path in the loop + if [[ -f $_BLESH_PATH ]]; then + + # Check if Blesh should be skipped + if [[ $_SKIP_BLESH = false ]]; then + + # If found, source ble.sh from the located path + builtin source "${_BLESH_PATH}" + + # Set the prompt end-of-line mark to a specific character + bleopt prompt_eol_mark='⏎' + + # Easier to read syntax highlighting for function names + ble-face -s syntax_function_name fg=171,bold + ble-face -s command_function fg=171 + ble-face -s varname_expr fg=171,bold + + # Bind 'C-d' to exit in ble.sh and suppress any output or error + ble-bind -x 'C-d' 'exit' > /dev/null 2>&1 # CTRL+d to exit + + # Create an alias to load/reload ble.sh + alias blesh="ble-reload" + else + # Create an alias to load/reload ble.sh + alias blesh="builtin source ${_BLESH_PATH} && bleopt prompt_eol_mark='⏎' && ble-face -s syntax_function_name fg=171,bold && ble-face -s command_function fg=171 && ble-face -s varname_expr fg=171,bold && ble-bind -x 'C-d' 'exit' > /dev/null 2>&1" + fi + + # Exit the loop as ble.sh has been found and sourced + break + fi +done + +# Clean up +unset _BLESH_PATH +unset _BLESH_PATHS + +####################################################### +### HISTORY DEFAULTS +####################################################### + +# If missing, recreate a new empty history file so apps don't show errors +if [[ -z ${HISTFILE+x} ]]; then + [[ ! -f "${HOME}/.bash_history" ]] && touch "${HOME}/.bash_history" +else + [[ ! -f "$HISTFILE" ]] && touch "$HISTFILE" +fi + +# Turn off bash history completely +# set +o history + +# Enable history expansion with space +# E.g. typing !! will replace the !! with your last command +bind Space:magic-space + +# Huge history. Doesn't appear to slow things down, so why not? +export HISTFILESIZE=100000 +export HISTSIZE=${HISTFILESIZE} + +# Avoid duplicate lines in the history and do not add lines that start with a space +export HISTCONTROL=ignoreboth:erasedups + +# Append to history instead of overwriting it so if you start a new terminal, you have old session history +shopt -s histappend histverify + +# Save and reload the history after each command finishes +# WARNING: Some of these mess up the history counter and is slower as history grows larger +# WARNING: It can also break some more advanced installed prompts +# This has issues with McFly so only set if it's not installed +if ! hascommand --strict mcfly; then + ## Append new history to history file, clear internal history list, and re-read the history file + export PROMPT_COMMAND="history -a; history -c; history -r; ${PROMPT_COMMAND}" + ## -Or- just record each line as it gets issued but new history is not in other sessions (faster) + #export PROMPT_COMMAND="history -a; ${PROMPT_COMMAND}" +fi + +# Save multi-line commands as one command +shopt -s cmdhist + +# Consecutive duplicate commands, invocations of common commands like ls without parameters, +# plus calls to the bg, fg and exit built-ins will not be appended to the history list +export HISTIGNORE='&:[ ]*:ls:ll:[bf]g:history:clear:cls:exit' + +# Use standard ISO 8601 timestamp +# %F equivalent to %Y-%m-%d +# %T equivalent to %H:%M:%S (24-hours format) +export HISTTIMEFORMAT='%F %T ' + +# Allow CTRL+S for history navigation (with CTRL+R) +stty -ixon + +####################################################### +# Terminology is a graphical EFL terminal emulator that can run in TTY sessions +# To split the window horizontally press Ctrl+Shift+PgUp +# To split the window vertically press Ctrl+Shift+PgDn +# To create Tabs press Ctrl+Shift+T and cycle through using Ctrl+1-9 +# Link: https://github.com/borisfaure/terminology +# Link: https://linoxide.com/terminology-terminal/ +####################################################### + +if [[ $_SKIP_TERMINOLOGY_TTY = false ]] && hascommand --strict terminology; then + # If we are in a TTY window , not in TMUX, and not logged in via SSH... + if [[ "$(tty)" =~ /dev/tty ]] && [[ ! "$TERM" =~ screen ]] && [[ -z "$SSH_CLIENT" ]] && [[ -z "$SSH_TTY" ]]; then + + # If TMUX is installed and set to load at TTY + if [[ $_TMUX_LOAD_TTY = true ]] && hascommand --strict tmux; then + + # Get the default session name + if [[ -z "${_TMUX_LOAD_SESSION_NAME}" ]]; then + if [[ "$(tmux list-sessions 2> /dev/null | wc -l)" -gt 0 ]]; then + _TMUX_LOAD_SESSION_NAME="" + else + _TMUX_LOAD_SESSION_NAME="$(whoami)" + fi + fi + + # Create the TMUX session if it doesn't exist + TMUX='' tmux -u new-session -d -s "${_TMUX_LOAD_SESSION_NAME}" 2> /dev/null + terminology --fullscreen --borderless --256color --exec "tmux attach -t ${_TMUX_LOAD_SESSION_NAME}" + + # No TMUX + else + terminology --fullscreen --borderless --256color && exit + fi + + fi + +####################################################### +# Automatically launch TMUX if this is a TTY Console or SSH session +# Most terminals can launch TMUX and automatically exit when TMUX is detached: +# konsole -e 'tmux new-session -A -s main' +# xfce4-terminal -e 'tmux new-session -A -s main' +# gnome-terminal -e 'tmux new-session -A -s main' +# kitty sh -c "tmux new-session -A -s main" +# terminology --exec "tmux new-session -A -s main" +# guake -e tmux +# Yakuake Profile -> Command -> /bin/bash -c "tmux new-session -A -s main" +# Alacritty: https://github.com/alacritty/alacritty/issues/2956 +# ssh user@server -t tmux new-session -A -s main +####################################################### + +# If TMUX is installed... +elif hascommand --strict tmux; then + + # We're in a TTY terminal... + if [[ "$(tty)" =~ /dev/tty ]] && [[ ! "$TERM" =~ screen ]]; then + [[ $_TMUX_LOAD_TTY = true ]] && tm + + # We're logged in via SSH... + elif [[ -n "$SSH_CLIENT" ]] || [[ -n "$SSH_TTY" ]]; then + [[ $_TMUX_LOAD_SSH = true ]] && tm + + # We are local and not using SSH or TTY... + else + [[ $_TMUX_LOAD_LOCAL = true ]] && tm + fi +fi + +####################################################### +# Tilix VTE Configuration +# Link: https://gnunn1.github.io/tilix-web/manual/vteconfig/ +####################################################### + +if [[ $TILIX_ID ]] || [[ $VTE_VERSION ]]; then + if [[ -f /etc/profile.d/vte.sh ]]; then + source /etc/profile.d/vte.sh + fi +fi diff --git a/dot_bashrc_help b/dot_bashrc_help new file mode 100644 index 0000000..4311aeb --- /dev/null +++ b/dot_bashrc_help @@ -0,0 +1,987 @@ +#! /usr/bin/less -frnS +====================================================================== + ┏━╸╻ ╻╺┳╸┏━┓┏━╸┏┳┓┏━╸ ╻ ╻╻ ╺┳╸╻┏┳┓┏━┓╺┳╸┏━╸ ┏┓ ┏━┓┏━┓╻ ╻┏━┓┏━╸ + ┣╸ ┏╋┛ ┃ ┣┳┛┣╸ ┃┃┃┣╸ ┃ ┃┃ ┃ ┃┃┃┃┣━┫ ┃ ┣╸ ┣┻┓┣━┫┗━┓┣━┫┣┳┛┃ + ┗━╸╹ ╹ ╹ ╹┗╸┗━╸╹ ╹┗━╸ ┗━┛┗━╸ ╹ ╹╹ ╹╹ ╹ ╹ ┗━╸ ╹┗━┛╹ ╹┗━┛╹ ╹╹┗╸┗━╸ + Extreme Ultimate .bashrc File Help + https://sourceforge.net/projects/ultimate-bashrc/ + bashrcupdate - Update your ~/.bashrc and help file from SourceForge +====================================================================== + Press / to search. Press n or SHIFT-N (backwards) to repeat. +  + Color Code Legend  + + Title + Binary or native command + Alias or function (from the Extreme Ultimate .bashrc file) + Parameter or options + Description +  + Bash Shortcuts  + +edit [file] - Edit a file using the default text editor +sudoedit [file] - Edit a file using sudo in a secure method +CTRL-X-E - Edit command in an editor (fc will edit last history) + +CTRL-Y - Pastes text from the Bash clipboard +CTRL-U - Cuts text from the cursor to the beginning of the line +CTRL-K - Cuts text from the cursor until the end of the line +CTRL-W or ALT-BACKSPACE - Cut word behind cursor +END CTRL-U or CTRL-Q - Cuts and clears the entire line + +CTRL-A or HOME - Move cursor to the beginning of the line +CTRL-E or END - Move cursor to end of line +CTRL-] x - Moves the cursor forward to next occurrence of x +ALT-CTRL-] x - Moves the cursor to previous occurrence of x +CTRL-XX - Toggle between start of line and current cursor position + +ALT-T - Swaps the last two words +CTRL-T - Swaps the last two characters +ALT-U - Capitalize all characters in a word after the cursor +ALT-L - Lower case all characters in a word after the cursor +ALT-. - Use the last word of the previous command + +history - Shows your command line history +!10 - Runs the command number listed by the history command +Up Arrow or CTRL-P - Previous command in history +Down Arrow or CTRL-N - Next command in history +CTRL-S - Searches the history forward +CTRL-R - Searches the history backward (reverse search) +ALT-R - Undo any changes to a command from history +CTRL-G - Leave the history searching mode without running the command +!! - Repeats the last command (i.e. sudo !!) +!$ - Repeats only the argument from the last command +!* - Repeats all parameters from previous command +!^ - Repeats only the first argument from the last command +!abc - Execute last command in history beginning with abc +!abc:p - Prints last command in history beginning with abc +echo "!!" > [file.sh] - Create a script of the last command +^find^replace - Repeats the last command but replaces text +mv /path/to/file.{txt,xml} - Brackets repeat the command with comma delimited changes +repeat - Repeat a command n times + +CTRL-C - Halts the current command +CTRL-Z - Stops the current command (resume with fg in foreground and bg in background) +CTRL-S - Pause terminal output (program will keep running) +CTRL-Q - Release terminal output (after being paused) +CTRL-L - Clear the screen +CTRL-D - Log out + +\ - Use at the end of a line to continue a multi-line command + or use at the beginning of a command to bypass an alias. +CTRL-ALT-E - Expand an alias to it's actual code + +To stack multiple commands on one line: +&& - Commands separated by a double ampersand means AND and runs multiple + commands synchronously with each one running only if the last did not fail. +& - A single ampersand runs multiple commands asynchronously (at the same time). +|| - Commands separated by a double pipe means OR and runs multiple + commands synchronously with each one running only if the last command failed. +(cd /tmp && ls) - jump to directory, execute command, jump back +  + Install and Updates  + +These aliases detect and use installed package managers on various distros: +has [package] - Show if a package is installed and it's information and version +pkglist - List all installed packages +pkgsearch [package] - Searches for a package +pkginstall [package] - Installs a package +pkgremove [package] - Removes a package +pkgclean [package] - Removes orphans or unused packages +pkgupdate [package] - Updates an installed package +pkgupdateall - Update the system and all packages +  + Information and Utility  + +hlp - Show this help information (also CTRL-H) +a - Show a list of available aliases and functions +alias [alias] - Give the command(s) for an alias + +ver - Show the version of the OS and kernel +lsb_release -a or uname -r - Show the current version of the operating system +uname -a - Show kernel config +cat /proc/cpuinfo or cpuinfo - Show CPU information +cat /proc/meminfo - Show memory information +usb - Show the USB device tree +pci - Show the PCI device tree +windowinfo - Select a window for information like geometry, class name, etc. +man [command] - Show manual for a command +mostused - See what command you are using the most +man ascii - ASCII table +apropos - Find word in man pages + +type -t [command] or check [command] - Show if a command is aliased, a file, or a built-in command +which [app] - Show which app will be run by default +whereis [app] - Show possible locations of app + +date - Show current date and time +today - Show the date only +now - Show the time +cal - Show this month's calendar +cal -3 - Show three months spanning the date +cal 1989 - Show a calendar for a specific year +stopwatch - Stop watch +countdown [time] - Display a countdown timer (requires termdown) +timer [time] - Simple timer function (requires termdown) +filetimenow [file] - Change file accessed and modified time to now +uptime - Show uptime since last start + +xdg-open or open - Launch a document/file/url in it's default X application +runfree - Start a program but immediately disown it and detach it from the terminal +glow [file.md] - Markdown viewer (if installed) +glowsafe [file.md] - Markdown viewer without internet (only local) +csvview - View any CSV file in the terminal +json [file] - View JSON files in the terminal with syntax highlighting +rot13 - Rot13 conversion +shellcheck - Check shell script syntax +  + Files and Folders  + +ls - Directory listing +ls -al - Formatted directory listing with hidden files +ls -d */ - List only the directories + +ll  - Directory listing: long listing format +labc - Directory listing: alphabetical sort +lx  - Directory listing: sort by extension +lk  - Directory listing: sort by size +lt  - Directory listing: sort by date +lc  - Directory listing: sort by change time +lu  - Directory listing: sort by access time +lw  - Directory listing: wide listing format +lm  - Directory listing: pipe through 'more' +lr  - Directory listing: recursive ls +l.  - Directory listing: only show hidden files +lf  - Directory listing: files only +ltree - Directory listing: tree format +ldir - Directory listing: directories only +new  - Directory listing: recently created/updated files +llfs +10k - List all files larger than a given size + +.. - Go back 1 folder +... - Go back 2 folders +.... - Go back 3 folders +..... - Go back 4 folders +..2 - Go back 2 folders +..3 - Go back 3 folders +..4 - Go back 4 folders +..5 - Go back 5 folders +up - go up a specified number of folders + +cd [directory] - Change directory +cd or cd ~ - Change to home +cd - - Change into the previous folder +pwd - Shows the directory you’re currently in +pwd- - Shows the directory you were previously in +pwdtail - Returns the last 2 fields of the working directory + +cp [file1] [file2] - Copy file 1 to file 2 +cpp - Copy file with a progress bar +cpg - Copy and go to the directory + +mv [file1] [file2] - Rename file 1 to file 2 +mv filename.{old,new} - Quickly rename file extension +mvg - Move and go into a directory +mkdir [directory] - Create directory +mkdir -p [a/long/path] - Create directory and all subdirectories +mkdirg - Create and go to the directory +rm [file] - Delete file +rm -f [file] - Force remove file +rm -rf [folder] or rmd [folder] - Remove a directory and all contents +ln -s [file] [link] - Create symbolic link to file +touch [file] - Create or update file +> [file] - Empty a file +cat > [file] - Place standard input into file +cat [file] - Output the contents of file +more [file] - Output the contents of file with paging +less [file] - Output the contents of file with paging +head [file] - Output first 10 lines of file +head -n [count] [file] - Output the first specified number of lines +tail [file] - Output last 10 lines of file +tail -n [count] [file] - Output the last specified number of lines +tail -f [file] - Output contents of file as it grows +diff [file1] [file2] or colordiff - Compare two files + +fullpath - Shows full path of file or wildcard +path - List the PATH environment variable directories +pathappend - Add a directory to the end of the path if it exists +pathprepend - Add a directory to the beginning of the path if it exists +df - Show disk usage +du - Show directory space usage +du -sh - Show human readable size in GB +du -s * | sort -n | tail - Show 10 biggest files in the current directory +diskspace - List all folders disk space sorted by largest space +folders - List disk space of immediate folders one level deep +totalsize [directory] - Show the size of the current or specified folder +blocksize [device] - Show the block size of a device +countfiles - Count all files (recursively) in the current folder +tree - Show a folder and file tree +treed - Show a folder tree +trash - Send file(s) to the trash (works with most desktop environments) +trashlist - Display the contents of the trash +trashempty - Empty and permanently delete all the files in the trash +rmempty [directory] - Recursively remove all empty directories +bak [file] - Create a timestamped backup of a file or directory +unbak [file] - Restore a backup using an interactive picker +clone [source] [dest] - Copy files recursively preserving all attributes +clonesync [source] [dest] - Clone and sync directories (deletes files not in source) +shred - Shred a specified file + +lsblk - List the available block devices +mount | column -t - Display mounted filesystems in nice layout +mount or m - Mount a file system +umount or um - Unmount a file system +dd if=[file.iso] of=[/dev/usb_drive] status=progress - Make a bootable USB from an ISO file + +pandoc [input.md] -o [output.html] - Convert markdown to HTML (Auto-detects extension) +pandoc [input.md] -o [output.pdf] - Convert markdown to PDF (Auto-detects extension) +pandoc -f html -t epub3 -o [output.epub] 'https://somedomain/page' - Convert an HTML page to ebook + +home - Go to your home folder +web - Change into Apache web folder +  + Searching  + +find . -type f [search] - Search filenames in the current folder +find . -type f -iname [search] or f [search] - Search filenames in the current folder and ignore case +find . -type f -name *.txt or f *.txt - Search for all text files in the current folder +find . -mtime 10 - Search filenames modified in the last 10 days +find . -cmin -60 - Search filenames modified in the last hour +find . -size +10 -size -50 - Search filenames between 10MB and 50MB +find /dir -type f -name *.mp3 -size +10M -exec rm {} \; - Find specified files and delete them with -exec +find . -type d -iname [search] or finddir [search] - Search directories in the current folder +find . -name '.directory' -type f -delete - Delete Dolphin's hidden .directory files recursively +find [/source/dir] -iname "[files.ext]" -exec cp {} [/target/dir] \; - Copy files from multiple folders into a single folder + +findtext [search] - Searches for text in all files in the current folder +findcode [search] - Searches for text in only code files (like HTML, PHP, JS, etc) +fzf - Fuzzy file finder (must be installed) +fzfpreview - Fuzzy file finder with preview +h [search] - Search command line history (also CTRL-S and CTRL-R) +grep [pattern] [files] - Search for pattern in files +grep -r [pattern] [directory] - Search recursively for pattern +[command] | grep [pattern] - Search for pattern in the output of command +pgrep [search] - Find process by name +sed -i 's/[findtext]/[replacetext]/g' [file] - Replace text in a file + +locate - Fastest file search using mlocate package +updatedb - Update the mlocate file location database +ulocate - Update the database before locating a file +locate -c or locount - Display the number of matching entries +findcronjob - Interactively search for cron jobs matching time criteria +findservice - Interactively search and manage systemd services +findlog - Shows log files with previews and returns filename (requires fzf) +preview - Shows files in directory with previews for editing (requires fzf) +  + Graphics Video and Fonts  + +xrandr -s 1920x1080 - Change resolution (careful with multi-monitor) +fawkes - Application that distorts images to protect your identity from facial recognition (if installed) + +colors - Print a list of colors with escape codes +colors256 - Print a list of all 256 color codes +colors24bit - Test for 24 bit true color in the terminal +whichdisplay - Shows the current display server (X11 or Wayland) +grabvideo - Video capture the Linux desktop (requires ffmpeg) +compressimage [image] - Convert an image to compressed jpg format +sparkbars - Draw spark “EQ” type bars across the terminal +sparkbars | lolcat - Draw spark “EQ” type bars across the terminal in color + +chafa [image.gif] - View an image or video in the console (if installed) +jp2a --color [image.jpg] - Create an ASCII version of an image (if installed) +figlet -w [width] -f [font] [text] - Create ASCII text (if installed) +toilet -w [width] -f [font] [text] - Create large ASCII text (if installed) +toiletfont - List the figlet/toilet font directory +toiletfontlist - Show all the fonts available + +ffmpeg -hwaccel cuvid -c:v h264_cuvid -i input.mp4 -ss 1:00 -to 2:00 -c:v h264_nvenc -preset:v slow -profile:v high -rc vbr_hq -b:v 0 -cq 30 -c:a copy -movflags +faststart output.mp4 - Trim Video +ffmpeg -i input.avi -vn -acodec copy output.aac - Extract Audio from Video +ffmpeg -i input.mp4 -c copy -an output.mp4 - Strip Video's Audio +ffmpeg -y -i input.ts -vcodec copy -acodec copy -map 0:v -map 0:a output.mp4 - Convert TS file to MP4 +ffmpeg -f x11grab -s wxga -r 25 -i :0.0 -sameq [/tmp/output.mpg] - Capture video of Linux desktop +  + Permissions  + +chmod u=rwx,g=rwx,o=rwx [file] - Change permission of file for user, group, other +chmod [octal] [file] - Change permission of file using an octal + +Octal order is user/group/other: +4 - read (r) +2 - write (w) +1 - execute (x) + +Common permission octals for user/group/other: +000  No permissions +600 rw- --- --- Owner only +644 rw- r-- r-- Owner only write access +660 rw- rw- --- Owner and group only +664 rw- rw- r-- Standard file with world read only +666 rw- rw- rw- Everyone read write access +755 rwx r-x r-x Executable but owner only write access +775 rwx rwx r-x Standard executable or directory (directories require executable) +777 rwx rwx rwx Everyone full access + +Permissions in the ls -l (long listing format) output: +Type (- Regular File, d Directory, l Symbolic Link) +|User permissions start here +|| Group permissions start here +|| | World (everyone else) permissions start here +|| | | User owner +|| | | | Group owner +VV V V V V +-rwxr-xr-x 1 root root 1071664 Jan 12 34:56 /usr/bin/bash + ^^^  ^ + |||  - Permission not set + ||x Execute permission + |w Write permission + r Read permission + +chmodcalc - Chmod calculator +chmoddirs [octal] - Recursively change only folder permissions +chmodfiles [octal] - Recursively change only file permission +fixuserhome [optional user] - Repairs and sets proper permissions of the home directory +chfix - Recursively set permissions of code files and directories +chmodcopy [source] [target] - Copy permissions from one file to another +chmod +x [file] or mx - make files executable +chmod ug+x ~/{.,}*.sh - Set execute with wildcard including all visible and invisible .sh files +chmod --reference [file1] [file2] - Copy permissions of file 1 to file 2 +chmod g+s [folder] - Ensure that new content in the directory will inherit group ownership +ls -lah $(which [command]) - Show the location of a command and it's attributes +  + Archives and File Compression  + +Tar flags: +c - Create archive j - Use bzip2 compression +t - Table of contents k - Do not overwrite +x - Extract T - Files from file +f - Specifies filename w - Ask for confirmation +z - Use gzip compression v - Verbose +p - Preserve pemissions X - Exclude + +tar -jcvf [file.tar.bz2] [file1] [file2] - Create a bzip2 archive file of file(s) +tar -jcvpf [file.tar.bz2] [/folder1] [/folder2] - Create bzip2 from folder(s) +pbzip2 --compress --best --force --verbose [files] - Create bzip2 with parallel processors +tar -xvjf [file.tar.bz2] [files] - Extract a bzip2 archive file +tar -jtvf [file.tar.bz2] - View contents of a tar.bz2 file + +tar -zcvf [file.tar.gz] [file1] [file2] - Create a gzip archive file of file(s) +tar -zcvpf [file.tar.gz] [/folder1] [/folder2] - Create gzip from folder(s) +tar -I 'pigz --best' -c -f [file.tar.gz] [files] - Create gzip with parallel processors +tar -xvzf [file.tar.gz] [files] - Extract a gzip archive file +tar -ztvf [file.tar.gz] - View contents of a tar.gz file + +tar -cvf [file.tar] [files] - Create a tar file +tar -xvf [file.tar] [files] - Extract a tar file +tar -tvf [file.tar] - View contents of a tar file + +zip [file.zip] [file1] [file2] - Create a zip file +zip -r [file.zip] [/folder1] [/folder2] - Create a zip from folder(s) +zip -9 -e -r [file.zip] [/folder] - Create zip with password and max compression +unzip [file.zip] -d [/destination] - Unzip a zip archive file to a folder +unzip -l [file.zip] or zipinfo [file.zip] - View contents of a zip file + +gzip [file] - Compress file and rename to file.gz +gzip -d [file.gz] - Decompress file + +extract - Extract any archive(s) +mkbz2 - Create a bzip2 archive file +unbz2 - Extract a bzip2 archive file +mkgz - Create a gzip archive file +ungz - Extract a gzip archive file +mktar - Create a tar file +untar - Extract a tar file + +encrypt - Encrypt a file using OpenSSL AES 256bit Cipher Block Chaining encryption +decrypt - Decrypt a file using OpenSSL AES 256bit Cipher Block Chaining encryption +  + Process Management and Memory Usage  + +top or htop or btm - Monitor processes and system resource usage +cpu - Show the top 10 CPU processes +free - Display amount of free and used memory in MB +activewinpid - Get active X-window process ID (3 second delay) + +ps - Display currently active processes +ps aux - Display active processes with more detail +ps aux | sort -nk +4 | tail - Top 10 running processes sorted by memory usage +ps aux | grep -i [process] - Find a process by name +smash - Kill process by name (interactive) +kill [pid] - Kill process with specified pid +kill -9 [pid] - Kill process with specified pid (forced) +kill -15 [pid] - Kill process with specified pid (graceful) +killall [proc] - Kill all processes named proc + +jobs -l - show all jobs +fg [number] - Brings job number to foreground +fg - Bring most recent stopped job to foreground +bg - Lists stopped/background jobs, resume stopped job in background +  + Networking  + +curl [http://server.com/file] - Download a file with curl +curl --output [destination_file] [http://server.com/file] - Download with curl to a specified output +wget [file] - Download file with wget +wget -O [destination_file] [http://server.com/file] - Download with wget and specify output +wget -c [file] - Continue stopped download +wget -r [url] - Recursively download files from URL +wget -qO- icanhazip.com - Get your external IP +download [url] - Download a file +youtube-dl -i --no-overwrites --write-description --write-info-json -f 'bestvideo+bestaudio/best' 'https://www.youtube.com/watch?v=' - Download Youtube Video + +ping [host] - Test the reachability of a host +fastping - Do not wait for ping interval 1 second (go fast) +ping -i 6O -a [IP] - Set audible alarm for when IP comes online +dig [domain] - Get DNS for domain +dig -x [host] - Reverse lookup host + +lsof -i or netwatch - Watch real time network activity +ports - Show open ports +iplocal - Get local IP addresses +ipexternal - Get outside external IP addresses +vim scp://[username@host]/[/path/to/somefile] - remotely edit a file +python -m SimpleHTTPServer - Serve current directory tree at http://HOSTNAME:8000/ +net rpc shutdown -I IPAddressOfWindowsPC -U username%password - Remotely shutdown windows machine +sleep 10; alert - Network notify alert for long running commands +  + SSH Remote Networking  + +ssh [user]@[host] - Connect to host as user +ssh -p 22 [user]@[host] - Connect using specified port +ssh -D [port] [user]@[host] - Connect and use bind port +ssh -t [reachable_host] ssh [unreachable_host] - SSH through host in the middle +ssh [user]@[host] -t tmux new-session -A -s main - Connect to host as user and start tmux +sshpass -p '[password]' ssh [user]@[host] - Connect to host as user with password in plaintext +scp [/path/to/local/file] [user]@[host]:[/path/to/remote/directory/] - Copy a local file to a remote host +scp [user]@[host]:[/path/to/remote/file] [/path/to/local/directory/] - Copy a file from a remote host to a local directory +sshfs [name@server:/path/to/folder] [/path/to/mount/point] - Mount file system over SSH +rsync -avzhP [/local/folder/] [user]@[server]:[/remote/folder/] - Sync local to SSH server (one way) +sync2ssh [local] [user@host:port] [remote] - Synchronize files using rsync over SSH + +ssh-keygen -t rsa -b 4096 - Generate a new SSH key pair with 4096 bits +ssh-copy-id [user]@[host] - Upload public key for passwordless secure SSH +  + Restarting  + +rebootsafe - reboot safely +rebootforce - force reboot +rebootlater [time] - Schedule the computer to auto reboot +rebootfast - Fast reboot user space without rebooting kernel +logout - Log out the current user +firmwareupdate - Update system firmware + +sudo shutdown -r now - reboot immediately +sudo shutdown -P now - power down immediately +sudo shutdown -r 10 - reboot after 10 minutes +sudo shutdown -r 12:34 - reboot after 12 hours and 34 minutes +sudo reboot -f - force reboot +last reboot | head - show last 10 reboots +  + Compile and Install From Source  + +./configure (may not have this) +make +make install +make clean +  + Configuration  + +ebrc - Edit the .bashrc file + +sudo nano /etc/fstab - Edit startup configuration +nano /etc/sysctl.conf - Change system security + +sudo systemctl restart httpd - Restart Apache +httpd -t or sudo apachectl configtest - Check the Apache configuration for errors +httpd -V - Find apache configuration file(s) +apacheconfig - Edit Apache web server configuration +ngconfig - Edit Nginx web server configuration +mysqld --verbose --help | grep -A 1 "Default options" - Find MySQL configuration file(s) +mysqlconfig - Edit MySQL database configuration +php -r 'echo php_ini_loaded_file() . "\n";' - Find the PHP configuration file +phpconfig - Edit the PHP configuration +phpchecksyntax - Check the syntax of a PHP file for errors +hoststoggle - Toggle the hosts file off and back on + +chattr +i /etc/php.ini - Protect a file +chattr -i /etc/php.ini - Unprotect a file + +logs - Show all logs in /var/log + +sudo cpupower frequency-info - Info about powersaving mode (requires cpupower or linux-cpupower package) +sudo cpupower frequency-set -g performance - Set powersaving mode off for performance +sudo cpupower frequency-set -g powersave - Set powersaving mode on to save power +  + Modify GRUB Boot Loader  + +sudo nano /etc/default/grub +sudo update-grub +  + Start/Stop/Restart a Service (systemd)  + +sudo systemctl start [service] or servicestart - Start a service +sudo systemctl stop [service] or servicestop - Stop a service +sudo systemctl kill [service] or servicekill - Force terminate a service +sudo systemctl restart [service] or servicerestart - Restart a service +sudo systemctl reload [service] or servicereload - Reload the configuration for a service +sudo systemctl status [service] or servicestatus - Show the status of the service +sudo systemctl is-active [service] - Check if a service is running +sudo systemctl enable [service] - Enable a service to run at boot +sudo systemctl is-enabled [service] - Check if a service is configured to start automatically +sudo systemctl --failed or failed - Find what services have failed +sudo systemctl list-units --type=service or servicesall - List all services +services - List all running or failed services + +apacherestart - Restart the Apache web server (requires Apache to be installed) +ngrestart - Restart the Nginx web server (requires Nginx to be installed) +  + Cron Jobs (cronie Package)  + +sudo crontab -l - List all cron jobs +sudo -E crontab -e - Edit the crontab file preserving editor environment variables + +Each line has five time-and-date fields followed by a command and new line. +The fields are separated by spaces (fields cannot contain spaces). +The five time-and-date fields are as follows: + + Minute (0-59) + | Hour (0-23, 0 = midnight) + | | Day (1-31) + | | | Month (1-12) + | | | | Weekday (0-6, 0 = Sunday) + | | | | | Script file or executable + V V V V V V + 00 00 01 * * /directories/script.sh + +The cron log is found here: nano /var/log/cron +More info here: https://help.ubuntu.com/community/CronHowto + +Anacron directory locations: +/etc/cron.d +/etc/cron.daily +/etc/cron.hourly +/etc/cron.weekly +/etc/cron.monthly +  + Run Commands at Different Times  + +nohup [command] & - Run commands after you log out of an ssh session +Example: nohup wget http://server.com/file.zip & + +at [time] - Run a linux command at a specific time +Example: +at 10:38 PM Fri +at> echo 'hello' +at> CTRL + D +  + User Administration  + +cut -d: -f1 /etc/passwd - List all users and their information +sudo useradd [user] - Add a new user +sudo userdel -r [user] - Delete an existing user +sudo usermod -l [user] - Change a user's name +sudo chfn [user] - Change the finger information for a user +sudo passwd [user] - Change a user's password +genpw [length] - Generate passwords (optional parameter -s or +s for symbols) +spellcheck [text] - Spell check command line input (requires aspell or hunspell) +usermod -a -G wheel [user] - Make a user an administrator (add to wheel group) +usermod -a -G wheel,developers [user] - Make a user a developer (add to developer group) + +w - Shows information about the users currently on the machine and their processes +w [user] - Shows the information about a particular user +whoami - Show who are you logged in as +who - Shows information about users who are currently logged in +users - Shows the login names of the users currently on the system + +sudo chsh --shell /bin/bash [user] - Change the default shell for a user +sudo chsh --list-shells - Print the list of shells +  + Group Administration  + +groups - List groups +cut -d: -f1 /etc/group | sort - List all groups +sudo groupadd [group] - Add a new group +sudo groupdel [group] - Delete an existing group +grep -i [group] /etc/group - List all the users in a specified group +id -Gn [user] - List all the groups for a user +find / -group [group] - List all the files owned by a group +  + Clam AntiVirus  + +freshclam - Update clam definitions + +clamscan -ir [/var/www/html/] + +clamscan --recursive=yes --infected --log=[/tmp/virus_scan.txt] --move=[/tmp/quarantine/] + --scan-mail=yes --phishing-sigs=yes --phishing-scan-urls=yes [/var/www/html/] + +clamscan -ir / > [/tmp/virus_scan.txt] +  + Rootkit Checks  + +To update and run RKHunter: +sudo rkhunter --update +sudo rkhunter --check + +RKHunter keeps its log here: +/var/log/rkhunter/rkhunter.log + +chkrootkit - Another rootkit check program +  + Security Check With LinPEASS  + +After installing peass, you can usually find it here: +/usr/share/peass/linPEAS/linpeas.sh +  + Spectre Meltdown Checker  + +spectre-meltdown-checker - Check for Spectre Meltdown bugs + +For more information see: +https://github.com/speed47/spectre-meltdown-checker +  + BtrFS File System Utilities  + +sudo btrfs fi show or btrcheck - Check status of raid drives +sudo btrfs device stats [/directory] or btrstats - Show device statistics +sudo btrfs scrub start [/directory] or btrscrub - Start a scrub +sudo btrfs scrub cancel [/directory] or btrpause - Cancel or pause a scrub +sudo btrfs scrub resume [/directory] or btrresume - Resume a paused scrub +sudo btrfs scrub status [/directory] or btrstatus - Show status of a scrub +sudo btrfs filesystem defragment -vf [file] or btrdefragfile - Defrag a file +sudo btrfs filesystem defragment -rvf [/directory] or btrdefragdir - Defrag a directory +  + Tmux Terminal Multiplexer  + +Load new session named "main" or attach to this session if it already exists: +tmux a -t main || tmux new -s main +-or- use the alias: tm + +tmux detach or tmd or CTRL-b + d - Detach from a Tmux session but leave the session running +tmux ls or tmlist - List all Tmux sessions +tmux list-sessions -F "#{session_name}" - List all Tmux sessions by name only +tmux lsc or tmclients - List all Tmux clients +tmux new -s [session] or tmnew [session] - Create a new session +tmux a -t [session] or tmattach [session] - Attach to a specified Tmux session (-d will detach all other clients) +tmux attach - Attach to an existing Tmux session +tmux rename -t [session] [new_name] or tmrename [session] [new_name] - Rename a Tmux session +tmux kill-session -t [session] or tmkill [session] - Kill a Tmux session +tmux kill-server or tmreset - Kill all Tmux sessions +  + Tmux Windows/Panes - Use CTRL-b First  + +0-9 - Select window by number +c - Create new window +w - List windows +f - Find window +, - Rename window +& - Kill window +l - Toggle last active window +p - Previous window +n - Next window + +% or \ - Split a pane into left and right panes +" or - - Split a pane into top and bottom panes +CTRL-o - Swap panes +SHIFT-{ - Move the current pane left +SHIFT-} - Move the current pane right +b - Break off pane to it's own window +z - Toggle zoom a window (great for copy) + +q - Show pane numbers +q 0-9 - Select pane by number +x - Close current pane (or use CTRL-D) +! - Close other panes except the current one + +PgUp - Scroll one page up and go into scroll mode +[ - Enter scroll mode +q - Quit scroll mode +] - Paste (any text selected is copied in mouse mode) + +To copy and paste in Tmux (using vim keybindings): +NOTE: This is set in the ~/tmux.conf file: set-window-option -g mode-keys vi +* Press the CTRL-b and then press [ to enter the copy mode +* If the mouse is enabled, you can just select the text +* If there is no mouse enabled, use the arrow keys for the start position and press CTRL-SPACE +* Use the arrow keys to the end position and press CTRL-w or ALT-w +* To paste, press CTRL-b followed by ] + +TIP: When in copy mode, type / to search and n to find next. + +Here is a list of all the new keybindings in my .tmux.conf for easy reference: +https://sourceforge.net/projects/ultimate-bashrc/files/support/ +| Keybinding | Description | +|------------------|-------------------------------------------------------------| +| CTRL-a | Use either C-a or C-b as the default PREFIX key | +| PREFIX | | Split the window horizontally (keeping the current path) | +| PREFIX \\ | Split the window horizontally (keeping the current path) | +| PREFIX - | Split the window vertically (keeping the current path) | +| PREFIX _ | Split the window vertically (keeping the current path) | +| PREFIX = | Synchronize panes (on/off) | +| PREFIX CTRL-a | Switch windows by pressing C-a twice | +| PREFIX c | Create a new window now opens using the current path | +| PREFIX d | Detach client | +| PREFIX CTRL-d | Choose a client to detach | +| PREFIX CTRL-e | Edit the Tmux configuration file and then reload settings | +| PREFIX g | Split the pane into a grid of 4 panes (horizontal resize) | +| PREFIX G | Split the pane into a grid of 4 panes (vertical resize) | +| PREFIX h | Show the Extreme Ultimate .bashrc help file in a new pane | +| PREFIX CTRL-h | Show the Extreme Ultimate .bashrc README file in a new pane | +| PREFIX j | Join panes (show the numbers with CTRL-w) | +| PREFIX J | Send pane to another pane (show the numbers with CTRL-w) | +| PREFIX CTRL-l | Clear screen and the buffer history | +| PREFIX M | Toggle the mouse on and off | +| PREFIX CTRL-m | Prompt for keyword and open a man page for it in a new pane | +| PREFIX r | Reload the .tmux.conf configuration file | +| PREFIX s | Toggle the status bar on and off | +| PREFIX T | Make the current window the first window | +| PREFIX v | Paste from the top paste buffer | +| PREFIX V | List paste buffers | +| PREFIX CTRL-v | Choose which buffer to paste from | +| PREFIX w | Display visible indicator of each pane | +| PREFIX z | Toggle zoom (make a pane full screen) | +| PREFIX Tab | Switch to the next available window | +| PREFIX Shift-Tab | Switch to the most recently accessed window | +| ENTER | Copy selection to OS clipboard when in copy mode | +| Mouse Selection | Copy selection to OS clipboard (in copy mode, mouse drag) | +  + Git  + +git status --short --branch - Shows a short Git status +git status - Shows full Git status +git pull - Git pull (fetch and merge) +git push - Git push +git add [file] - Git add +git add -p [file] - Git add interactively choose hunks +git commit -m "Commit message" - Git commit +git reset - Git reset specifies a file to un-add (or no file to unstage all) +git log - Git log +git diff - Git diff +git diff --stat - Git diff stat +git diff --cached - Git diff cached +git checkout [branch] - Checkout a branch +git checkout -b [branch] - Create a new branch + +gg - Shows a short Git status or if given arguments invokes Git +ggg - Shows full Git status +ggs - Shows a short Git status with branch information +ggp - Git pull (fetch and merge) +ggf - Git fetch +ggm - Git merge +ggpu - Git push +ggr - Git reset specifies a file to un-add (or no file to unstage all) +gga [file] - Git add +ggap [file] - Git add interactively choose hunks +ggac "Commit message" - Git add all files in current path and commit +ggc "Commit message" - Git commit +ggca - Git commit amend +ggl - Git log with improved formatting +ggd - Git diff +ggds - Git diff stat +ggdc - Git diff cached +ggb [branch] - Checkout a branch +ggcb [branch] - Create a new branch +ggst - Git stash +ggpop - Git pop +gitls - Directory listing with Git status shown +lg - If installed, runs LazyGit +ggu - If installed, runs GitUI +gitc - If installed, runs Git Commander +gitt - If installed, runs Tig interface for Git +gitrv - If installed, runs GRV (Git Repository Viewer) +gitundo - If installed, runs Ugit Git Undo + +gitalias - Installs or updates many useful Git alias commands +Restart bash to see changes. Visit https://github.com/GitAlias/gitalias for more info + +  +  + Multimedia and Voice  + +say [text] - Text to speech +saygreet - Say a greeting using text to speech +sayclipboard - Read the clipboard out loud +yt [url] - Play YouTube videos +ytd [url] - Download a YouTube video with yt-dlp + +  + Text Manipulation  + +formattext [text] - Transform text using common string formatting operations +trim [text] - Remove leading and trailing whitespace from a string +fixspaces [text] - Replace multiple spaces, tabs, and underscores with single space +lowercase [text] - Convert a string to lowercase +uppercase [text] - Convert a string to uppercase +capitalize [text] - Capitalize every word in a string +titlecase [text] - Convert to title case (skip small words) +smartquotes [text] - Convert regular quotes to smart quotes +unsmartquotes [text] - Convert smart quotes to regular quotes +reversetext [text] - Reverse the characters in a string +countchars [text] - Count the number of characters in a string +countwords [text] - Count the number of words in a string +startswith [text] [prefix] - Check if a string starts with a specific substring +endswith [text] [suffix] - Check if a string ends with a specific substring +lines [file] [range] - Display specific lines or line ranges from a file +wrap [width] - Wrap text at specified width +unwrap - Remove line wrapping from text +fixfilename [file] - Clean up and standardize file names +replacetext [find] [replace] [file] - Replace text strings in files +  + Clipboard  + +clipboard - View, set, or clear clipboard content (supports piping) +cbshow - Show the contents of the clipboard +trimcb - Trim leading and trailing characters on clipboard +file2cb [file] - Load file contents to clipboard +cb2file [file] - Save clipboard contents to file +file2asc [file] - Compress file to clipboard as base64 +asc2file [file] - Extract base64 from clipboard to file +  + Boot Analysis  + +boottime - Show how long it took to boot the system +boottimelist - Show detailed boot time breakdown by service +bootchart - Generate a visual boot time analysis chart as SVG +bootmessages - View kernel messages from the current boot +bootprevious - View kernel messages from the previous boot +booterrors - Show warnings and errors from current boot +bootlist - List available boot sessions with IDs and timestamps +kernelmessages - Show raw kernel ring buffer messages (dmesg) +kernelerrors - Show filtered kernel errors, warnings, and critical issues +checkreboot - Check if the system needs to be rebooted +  + Scripting Utilities  + +ask "question" [Y/N] - Interactive yes/no prompt for scripts +hascommand [command] - Check if a command or alias exists (with caching) +hascommandclear - Clear the hascommand cache after installing software +timeelapsed [date] [message] - Show elapsed time since a given date +runwithfeedback [desc] [cmd] - Run command with visual progress feedback +analyzecode [file] - Analyze a code file to provide statistics +showfunctions [file] - List function names with line numbers from source code +createmenu - Create interactive text picker menu from piped input +  + Advanced User Management  + +createuser - Interactively create, configure, and test a new user +deleteuser [user] - Remove a user from the system +wipeuser [user] - Remove user and all traces including home directory +  + SSL and Security  + +sslcheck [domain] - Check SSL certificate expiration with color-coded urgency +flushcache - Clear RAM memory cache, buffer and swap space +flushdns - Clear DNS cache to refresh domain name data +ss -tuln - Show listening TCP and UDP ports +ss -p - Show process using each socket +fail2ban-client status - Show fail2ban status (if installed) +last -a - Show last logged in users with hostname +lastb - Show bad login attempts +  + Additional Archive Formats  + +mkzst [archive.tar.zst] [files] - Create a zstd compressed archive +unzst [archive.tar.zst] - Extract a zstd compressed archive +mkxz [archive.tar.xz] [files] - Create an xz compressed archive +unxz [archive.tar.xz] - Extract an xz compressed archive +mk7z [archive.7z] [files] - Create a 7z archive with ultra compression +un7z [archive.7z] - Extract a 7z archive +mkiso [archive.iso] [files] - Create an ISO image +uniso [archive.iso] - Extract an ISO image +mkzip [archive.zip] [files] - Create a zip with maximum compression +  + Common Linux Commands  + +System Information: +hostnamectl - Show or set system hostname and related settings +timedatectl - Show or set system time and date settings +localectl - Show or set system locale and keyboard settings +dmidecode - Show hardware information from BIOS (requires sudo) +lscpu - Display CPU architecture information +lsmem - List memory ranges and their online status +lspci -v - List all PCI devices with details +lsusb -v - List all USB devices with details +hdparm -I [/dev/sda] - Show detailed disk information +smartctl -a [/dev/sda] - Show SMART disk health data + +Disk and Filesystem: +blkid - Show block device attributes (UUID, type, label) +fdisk -l - List all disk partitions (requires sudo) +parted -l - List partitions on all devices (requires sudo) +lsblk -f - List block devices with filesystem info +findmnt - List all mounted filesystems in tree format +findmnt -t ext4,btrfs - List only specific filesystem types +iostat - Show CPU and disk I/O statistics +iotop - Top-like I/O monitor (requires sudo) +fstrim -av - Trim all mounted filesystems (SSD optimization) +sync - Flush filesystem buffers to disk + +Network Diagnostics: +ip a - Show all network interfaces and IP addresses +ip r - Show routing table +ip link - Show network interface status +ss -tuln - Show listening ports (replaces netstat) +nmcli device status - Show NetworkManager device status +nmcli connection show - Show NetworkManager connections +traceroute [host] - Trace packet route to host +mtr [host] - Combines ping and traceroute (interactive) +nmap -sn [192.168.1.0/24] - Scan network for live hosts +arp -a - Show ARP cache (MAC addresses on local network) +netstat -i - Show network interface statistics +vnstat - Show network traffic statistics over time +dig +short [domain] - Quick DNS lookup +host [domain] - Simple DNS lookup +nslookup [domain] - Query DNS servers interactively + +Process and System Monitoring: +strace -p [pid] - Trace system calls of a running process +ltrace -p [pid] - Trace library calls of a running process +lsof -p [pid] - List files opened by a process +lsof +D [directory] - Find processes using files in directory +fuser -v [file] - Show processes using a file or filesystem +pmap [pid] - Show memory map of a process +watch -n 1 [command] - Run command every second and display output +timeout [seconds] [command] - Run command with time limit +time [command] - Measure execution time of a command +nice -n 19 [command] - Run command with lowest priority +renice -n 10 -p [pid] - Change priority of running process +nproc - Show number of available CPU cores + +Journald Logs: +journalctl -xe - Show recent logs with explanations +journalctl -f - Follow new log entries (like tail -f) +journalctl -u [service] - Show logs for specific service +journalctl --since "1 hour ago" - Show logs from last hour +journalctl -p err - Show only error messages +journalctl -b -1 - Show logs from previous boot +journalctl --disk-usage - Show journal disk usage +cleansystemlogs [days] - Clear log entries older than specified days + +Miscellaneous: +xdg-mime query default [mimetype] - Show default app for file type +xdg-mime query filetype [file] - Show MIME type of file +xdg-settings get default-web-browser - Show default web browser +file [file] - Determine file type +stat [file] - Show detailed file information +namei -l [path] - Show path resolution with permissions +getfacl [file] - Show file access control lists +setfacl -m u:[user]:rwx [file] - Set ACL permissions +lsattr [file] - List file attributes (immutable, etc) +chattr +i [file] - Make file immutable (even root cannot modify) +chattr -i [file] - Remove immutable attribute +column -t - Format input into aligned columns +printf - Formatted printing (more control than echo) +seq [start] [end] - Generate sequence of numbers +shuf - Randomly shuffle lines of input +sort -u - Sort and remove duplicates +sort -h - Sort human-readable numbers (1K, 2M, 3G) +uniq -c - Count occurrences of unique lines +cut -d: -f1 - Extract first field using : delimiter +tr 'a-z' 'A-Z' - Translate lowercase to uppercase +xargs -I {} [cmd] - Build command from piped input +tee [file] - Write output to file and stdout +env - Show all environment variables +printenv [VAR] - Print specific environment variable +export [VAR=value] - Set environment variable +source [file] or . [file] - Execute commands from file in current shell +eval [string] - Evaluate string as shell command +exec [command] - Replace shell with command diff --git a/dot_config/fastfetch/config.jsonc b/dot_config/fastfetch/config.jsonc new file mode 100644 index 0000000..63b4160 --- /dev/null +++ b/dot_config/fastfetch/config.jsonc @@ -0,0 +1,84 @@ +// Inspired by Catnap +{ + "$schema": "https://github.com/fastfetch-cli/fastfetch/raw/dev/doc/json_schema.json", + "logo": { + "type": "small", + "padding": { + "top": 1 + } + }, + "display": { + "separator": " " + }, + "modules": [ + { + "key": "╭───────────╮", + "type": "custom" + }, + { + "key": "│ {#31} user {#keys}│", + "type": "title", + "format": "{user-name}" + }, + { + "key": "│ {#32}󰇅 hname {#keys}│", + "type": "title", + "format": "{host-name}" + }, + { + "key": "│ {#33}󰅐 uptime {#keys}│", + "type": "uptime" + }, + { + "key": "│ {#34}{icon} distro {#keys}│", + "type": "os" + }, + { + "key": "│ {#35} kernel {#keys}│", + "type": "kernel" + }, + { + "key": "│ {#36} wm {#keys}│", + "type": "wm" + }, + { + "key": "│ {#36}󰇄 desktop {#keys}│", + "type": "de" + }, + { + "key": "│ {#31} term {#keys}│", + "type": "terminal" + }, + { + "key": "│ {#32} shell {#keys}│", + "type": "shell" + }, + { + "key": "│ {#33}󰍛 cpu {#keys}│", + "type": "cpu", + "showPeCoreCount": true + }, + { + "key": "│ {#34}󰉉 disk {#keys}│", + "type": "disk", + "folders": "/" + }, + { + "key": "│ {#36} memory {#keys}│", + "type": "memory" + }, + { + "key": "├───────────┤", + "type": "custom" + }, + { + "key": "│ {#39} colors {#keys}│", + "type": "colors", + "symbol": "circle" + }, + { + "key": "╰───────────╯", + "type": "custom" + } + ] +} \ No newline at end of file diff --git a/dot_config/ghostty/config b/dot_config/ghostty/config new file mode 100644 index 0000000..f08fe0a --- /dev/null +++ b/dot_config/ghostty/config @@ -0,0 +1,4 @@ +background-opacity = 0.7 +font-family = Red Hat Mono +font-size = 22 +theme = Cyberpunk Scarlet Protocol diff --git a/dot_config/hyfetch.json b/dot_config/hyfetch.json new file mode 100644 index 0000000..61e4016 --- /dev/null +++ b/dot_config/hyfetch.json @@ -0,0 +1,15 @@ +{ + "preset": "femboy", + "mode": "rgb", + "auto_detect_light_dark": true, + "light_dark": "dark", + "lightness": 0.65, + "color_align": { + "mode": "horizontal" + }, + "backend": "fastfetch", + "args": null, + "distro": null, + "pride_month_disable": false, + "custom_ascii_path": null +} \ No newline at end of file diff --git a/dot_envrc b/dot_envrc new file mode 100644 index 0000000..d46bf43 --- /dev/null +++ b/dot_envrc @@ -0,0 +1 @@ +export EDITOR=vim diff --git a/dot_gvimrc b/dot_gvimrc new file mode 100644 index 0000000..99ca07a --- /dev/null +++ b/dot_gvimrc @@ -0,0 +1,85 @@ +" Hide menus +set guioptions-=T +set guioptions-=r +set guioptions-=l +set guioptions-=L +set guioptions-=R + +" Only remove menubar on non-windows platforms. +" The windows version looks a bit like ass without it, +" somehow, it develops ugly white borders around +" the bottom frame, I have no clue where they come from. +if !has("gui_win32") + set guioptions-=m +endif + +" Add Vim icon to window, where it is shown depends on platform, windowing +" system, X11 server depth, etc etc. +set guioptions+=i + +" Automagically yank to windowing system clipboard on visual select. +" This makes gvim behave like a normal unix application. +set guioptions+=a + +" Typography +if has("gui_gtk2") || has("gui_gtk3") + set guifont=Noto\ Mono\ for\ Powerline\ 12 +elseif has("gui_macvim") + set guifont=Menlo\ Regular:h14 +elseif has("gui_win32") + set guifont=Consolas:h11:cANSI +endif + +" Toggle fullscreen +" - Don't need it anymore; i3wm does it (Super+f). +" map :call system("wmctrl -ir " . v:windowid . " -b toggle,fullscreen") + +" Set initial window size +" set lines=36 columns=144 + +" Disable mouse +" set mouse=c + +" Colorscheme settings +" It is required to use the Atomic Color Scheme +" Source: https://github.com/gerardbm/vim-atomic +" function! AtomicSwitcher() +" if (strftime('%H') > 8) && (strftime('%H') < 20) +" AtomicSpaceMC +" else +" AtomicNightHC +" endif +" endfunction + +" call AtomicSwitcher() + +colorscheme atomic + +AtomicSpaceMC + +" Shortcuts +nnoremap :call CycleModes():colorscheme atomic +nnoremap 1 :AtomicSpaceHC +nnoremap 2 :AtomicSpaceMC +nnoremap 3 :AtomicSpaceLC +nnoremap 4 :AtomicNightHC +nnoremap 5 :AtomicNightMC +nnoremap 6 :AtomicNightLC +nnoremap 7 :AtomicLightHC +nnoremap 8 :AtomicLightMC +nnoremap 9 :AtomicLightLC +nnoremap :AtomicOnionHC +nnoremap :AtomicOnionMC +nnoremap :AtomicOnionLC +nnoremap :AtomicBloodHC +nnoremap :AtomicBloodMC +nnoremap :AtomicBloodLC +nnoremap :AtomicGrassHC +nnoremap :AtomicGrassMC +nnoremap :AtomicGrassLC +nnoremap :AtomicOceanHC +nnoremap :AtomicOceanMC +nnoremap :AtomicOceanLC + +" Paste ('p') from the clipboard +set clipboard=unnamedplus diff --git a/dot_vim/after/syntax/c.vim b/dot_vim/after/syntax/c.vim new file mode 100644 index 0000000..7f5af2d --- /dev/null +++ b/dot_vim/after/syntax/c.vim @@ -0,0 +1,4 @@ +syn match cFunction "\<[a-zA-Z_][a-zA-Z_0-9]*\>[^()]*)("me=e-2 +syn match cFunction "\<[a-zA-Z_][a-zA-Z_0-9]*\>\s*("me=e-1 + +hi cFunction ctermfg=12 diff --git a/dot_vim/after/syntax/css.vim b/dot_vim/after/syntax/css.vim new file mode 100644 index 0000000..55689e8 --- /dev/null +++ b/dot_vim/after/syntax/css.vim @@ -0,0 +1,3 @@ +syn match VendorPrefix /-\(moz\|webkit\|o\|ms\)-[a-zA-Z-]\+/ containedin=cssDefinition + +hi VendorPrefix ctermfg=3 cterm=italic diff --git a/dot_vim/after/syntax/csv.vim b/dot_vim/after/syntax/csv.vim new file mode 100644 index 0000000..bf0f87a --- /dev/null +++ b/dot_vim/after/syntax/csv.vim @@ -0,0 +1,29 @@ +if exists('b:current_syntax') + finish +endif + +let s:del_def = ',' +let s:del = '\%(' . s:del_def . '\|$\)' +let s:del_noend = '\%(' . s:del_def . '\)' +let s:col = + \ '\%(\%(\%(' . (s:del_def !~# '\s' ? '\s*' : '') . + \ '"\%(' . (exists("g:csv_nl") ? '\_' : '' ) . + \ '[^"]\|""\)*"\s*\)' . s:del . '\)\|\%(' . + \ '[^' . s:del_def . ']*' . s:del . '\)\)' +let s:col_end = '\%(\%(\%(' . (s:del_def !~# '\s' ? '\s*' : '') . + \ '"\%(' . (exists("g:csv_nl") ? '\_' : '' ) . + \ '[^"]\|""\)*"\)' . s:del_noend . '\)\|\%(' . + \ '[^' . s:del_def . ']*' . s:del_noend . '\)\)' +let s:del = get(b:, 'delimiter', s:del_def) + +exe 'syn match CSVColumnEven nextgroup=CSVColumnOdd /' + \ . s:col . '/ contains=CSVDelimiter' +exe 'syn match CSVColumnOdd nextgroup=CSVColumnEven /' + \ . s:col . '/ contains=CSVDelimiter' +exe 'syn match CSVDelimiter /' . s:col_end . '/ms=e,me=e contained' + +hi def link CSVDelimiter Number +hi def link CSVColumnOdd Define +hi def link CSVColumnEven Keyword + +let b:current_syntax = 'csv' diff --git a/dot_vim/after/syntax/html.vim b/dot_vim/after/syntax/html.vim new file mode 100644 index 0000000..6b86447 --- /dev/null +++ b/dot_vim/after/syntax/html.vim @@ -0,0 +1,3 @@ +syn match htmlEqual /=/ containedin=htmlTag + +hi htmlEqual ctermfg=15 diff --git a/dot_vim/after/syntax/markdown.vim b/dot_vim/after/syntax/markdown.vim new file mode 100644 index 0000000..9adceb8 --- /dev/null +++ b/dot_vim/after/syntax/markdown.vim @@ -0,0 +1,33 @@ +" Links +syn region markdownLinkText matchgroup=markdownLinkTextDelimiter + \ start="!\=\[\%(\_[^]]*]\%( \=[[(]\)\)\@=" end="\]\%( \=[[(]\)\@=" + \ nextgroup=markdownLink,markdownId skipwhite + \ contains=@markdownInline,markdownLineStart concealends +syn region markdownLink matchgroup=markdownLinkDelimiter + \ start="(" end=")" keepend contained conceal contains=markdownUrl +syn cluster markdownInline add=@NoSpell + +" Code +syn region markdownCode matchgroup=markdownCodeDelimiter + \ start="`" end="`" concealends + +" Extended +syn match markdownExt /{[.:#][^}]*}/ conceal + +" Error +" syn match markdownError "\w\@<=_\w\@=" +syn match markdownError "\w\@<=\w\@=" + +" Latex +let b:current_syntax = '' +unlet b:current_syntax +syn include syntax/tex.vim +syn region markdownTexBlock matchgroup=markdownTexMathDelimiter + \ start='^\$\$' end='\$\$$' contains=@texMathZoneGroup +syn match markdownTexLine '\$[^$].\{-}\$' contains=@texMathZoneGroup + +" Highlights +hi markdownTexMathDelimiter ctermfg=13 +hi markdownTexBlock ctermfg=13 +hi markdownTexLine ctermfg=13 +hi markdownExt ctermfg=11 cterm=none diff --git a/dot_vim/after/syntax/vim.vim b/dot_vim/after/syntax/vim.vim new file mode 100644 index 0000000..a1d4297 --- /dev/null +++ b/dot_vim/after/syntax/vim.vim @@ -0,0 +1,5 @@ +syn keyword vimKeywords1 if endif containedin=vimFuncBody +syn keyword vimKeywords2 try catch endtry containedin=vimFuncBody + +hi vimKeywords1 ctermfg=10 +hi vimKeywords2 ctermfg=9 diff --git a/dot_vim/autoload/plug.vim b/dot_vim/autoload/plug.vim new file mode 100644 index 0000000..de5ae1f --- /dev/null +++ b/dot_vim/autoload/plug.vim @@ -0,0 +1,2901 @@ +" vim-plug: Vim plugin manager +" ============================ +" +" 1. Download plug.vim and put it in 'autoload' directory +" +" # Vim +" curl -fLo ~/.vim/autoload/plug.vim --create-dirs \ +" https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim +" +" # Neovim +" sh -c 'curl -fLo "${XDG_DATA_HOME:-$HOME/.local/share}"/nvim/site/autoload/plug.vim --create-dirs \ +" https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim' +" +" 2. Add a vim-plug section to your ~/.vimrc (or ~/.config/nvim/init.vim for Neovim) +" +" call plug#begin() +" +" " List your plugins here +" Plug 'tpope/vim-sensible' +" +" call plug#end() +" +" 3. Reload the file or restart Vim, then you can, +" +" :PlugInstall to install plugins +" :PlugUpdate to update plugins +" :PlugDiff to review the changes from the last update +" :PlugClean to remove plugins no longer in the list +" +" For more information, see https://github.com/junegunn/vim-plug +" +" +" Copyright (c) 2024 Junegunn Choi +" +" MIT License +" +" Permission is hereby granted, free of charge, to any person obtaining +" a copy of this software and associated documentation files (the +" "Software"), to deal in the Software without restriction, including +" without limitation the rights to use, copy, modify, merge, publish, +" distribute, sublicense, and/or sell copies of the Software, and to +" permit persons to whom the Software is furnished to do so, subject to +" the following conditions: +" +" The above copyright notice and this permission notice shall be +" included in all copies or substantial portions of the Software. +" +" THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +" EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +" MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +" NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +" LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +" OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +" WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +if exists('g:loaded_plug') + finish +endif +let g:loaded_plug = 1 + +let s:cpo_save = &cpo +set cpo&vim + +let s:plug_src = 'https://github.com/junegunn/vim-plug.git' +let s:plug_tab = get(s:, 'plug_tab', -1) +let s:plug_buf = get(s:, 'plug_buf', -1) +let s:mac_gui = has('gui_macvim') && has('gui_running') +let s:is_win = has('win32') +let s:nvim = has('nvim-0.2') || (has('nvim') && exists('*jobwait') && !s:is_win) +let s:vim8 = has('patch-8.0.0039') && exists('*job_start') +let s:shell_error = 0 +if s:is_win && &shellslash + set noshellslash + let s:me = resolve(expand(':p')) + set shellslash +else + let s:me = resolve(expand(':p')) +endif +let s:base_spec = { 'branch': '', 'frozen': 0 } +let s:TYPE = { +\ 'string': type(''), +\ 'list': type([]), +\ 'dict': type({}), +\ 'funcref': type(function('call')) +\ } +let s:loaded = get(s:, 'loaded', {}) +let s:triggers = get(s:, 'triggers', {}) + +function! s:is_powershell(shell) + return a:shell =~# 'powershell\(\.exe\)\?$' || a:shell =~# 'pwsh\(\.exe\)\?$' +endfunction + +function! s:isabsolute(dir) abort + return a:dir =~# '^/' || (has('win32') && a:dir =~? '^\%(\\\|[A-Z]:\)') +endfunction + +function! s:git_dir(dir) abort + let gitdir = s:trim(a:dir) . '/.git' + if isdirectory(gitdir) + return gitdir + endif + if !filereadable(gitdir) + return '' + endif + let gitdir = matchstr(get(readfile(gitdir), 0, ''), '^gitdir: \zs.*') + if len(gitdir) && !s:isabsolute(gitdir) + let gitdir = a:dir . '/' . gitdir + endif + return isdirectory(gitdir) ? gitdir : '' +endfunction + +function! s:git_origin_url(dir) abort + let gitdir = s:git_dir(a:dir) + let config = gitdir . '/config' + if empty(gitdir) || !filereadable(config) + return '' + endif + return matchstr(join(readfile(config)), '\[remote "origin"\].\{-}url\s*=\s*\zs\S*\ze') +endfunction + +function! s:git_revision(dir) abort + let gitdir = s:git_dir(a:dir) + let head = gitdir . '/HEAD' + if empty(gitdir) || !filereadable(head) + return '' + endif + + let line = get(readfile(head), 0, '') + let ref = matchstr(line, '^ref: \zs.*') + if empty(ref) + return line + endif + + if filereadable(gitdir . '/' . ref) + return get(readfile(gitdir . '/' . ref), 0, '') + endif + + if filereadable(gitdir . '/packed-refs') + for line in readfile(gitdir . '/packed-refs') + if line =~# ' ' . ref + return matchstr(line, '^[0-9a-f]*') + endif + endfor + endif + + return '' +endfunction + +function! s:git_local_branch(dir) abort + let gitdir = s:git_dir(a:dir) + let head = gitdir . '/HEAD' + if empty(gitdir) || !filereadable(head) + return '' + endif + let branch = matchstr(get(readfile(head), 0, ''), '^ref: refs/heads/\zs.*') + return len(branch) ? branch : 'HEAD' +endfunction + +function! s:git_origin_branch(spec) + if len(a:spec.branch) + return a:spec.branch + endif + + " The file may not be present if this is a local repository + let gitdir = s:git_dir(a:spec.dir) + let origin_head = gitdir.'/refs/remotes/origin/HEAD' + if len(gitdir) && filereadable(origin_head) + return matchstr(get(readfile(origin_head), 0, ''), + \ '^ref: refs/remotes/origin/\zs.*') + endif + + " The command may not return the name of a branch in detached HEAD state + let result = s:lines(s:system('git symbolic-ref --short HEAD', a:spec.dir)) + return s:shell_error ? '' : result[-1] +endfunction + +if s:is_win + function! s:plug_call(fn, ...) + let shellslash = &shellslash + try + set noshellslash + return call(a:fn, a:000) + finally + let &shellslash = shellslash + endtry + endfunction +else + function! s:plug_call(fn, ...) + return call(a:fn, a:000) + endfunction +endif + +function! s:plug_getcwd() + return s:plug_call('getcwd') +endfunction + +function! s:plug_fnamemodify(fname, mods) + return s:plug_call('fnamemodify', a:fname, a:mods) +endfunction + +function! s:plug_expand(fmt) + return s:plug_call('expand', a:fmt, 1) +endfunction + +function! s:plug_tempname() + return s:plug_call('tempname') +endfunction + +function! plug#begin(...) + if a:0 > 0 + let home = s:path(s:plug_fnamemodify(s:plug_expand(a:1), ':p')) + elseif exists('g:plug_home') + let home = s:path(g:plug_home) + elseif has('nvim') + let home = stdpath('data') . '/plugged' + elseif !empty(&rtp) + let home = s:path(split(&rtp, ',')[0]) . '/plugged' + else + return s:err('Unable to determine plug home. Try calling plug#begin() with a path argument.') + endif + if s:plug_fnamemodify(home, ':t') ==# 'plugin' && s:plug_fnamemodify(home, ':h') ==# s:first_rtp + return s:err('Invalid plug home. '.home.' is a standard Vim runtime path and is not allowed.') + endif + + let g:plug_home = home + let g:plugs = {} + let g:plugs_order = [] + let s:triggers = {} + + call s:define_commands() + return 1 +endfunction + +function! s:define_commands() + command! -nargs=+ -bar Plug call plug#() + if !executable('git') + return s:err('`git` executable not found. Most commands will not be available. To suppress this message, prepend `silent!` to `call plug#begin(...)`.') + endif + if has('win32') + \ && &shellslash + \ && (&shell =~# 'cmd\(\.exe\)\?$' || s:is_powershell(&shell)) + return s:err('vim-plug does not support shell, ' . &shell . ', when shellslash is set.') + endif + if !has('nvim') + \ && (has('win32') || has('win32unix')) + \ && !has('multi_byte') + return s:err('Vim needs +multi_byte feature on Windows to run shell commands. Enable +iconv for best results.') + endif + command! -nargs=* -bar -bang -complete=customlist,s:names PlugInstall call s:install(0, []) + command! -nargs=* -bar -bang -complete=customlist,s:names PlugUpdate call s:update(0, []) + command! -nargs=0 -bar -bang PlugClean call s:clean(0) + command! -nargs=0 -bar PlugUpgrade if s:upgrade() | execute 'source' s:esc(s:me) | endif + command! -nargs=0 -bar PlugStatus call s:status() + command! -nargs=0 -bar PlugDiff call s:diff() + command! -nargs=? -bar -bang -complete=file PlugSnapshot call s:snapshot(0, ) +endfunction + +function! s:to_a(v) + return type(a:v) == s:TYPE.list ? a:v : [a:v] +endfunction + +function! s:to_s(v) + return type(a:v) == s:TYPE.string ? a:v : join(a:v, "\n") . "\n" +endfunction + +function! s:glob(from, pattern) + return s:lines(globpath(a:from, a:pattern)) +endfunction + +function! s:source(from, ...) + let found = 0 + for pattern in a:000 + for vim in s:glob(a:from, pattern) + execute 'source' s:esc(vim) + let found = 1 + endfor + endfor + return found +endfunction + +function! s:assoc(dict, key, val) + let a:dict[a:key] = add(get(a:dict, a:key, []), a:val) +endfunction + +function! s:ask(message, ...) + call inputsave() + echohl WarningMsg + let answer = input(a:message.(a:0 ? ' (y/N/a) ' : ' (y/N) ')) + echohl None + call inputrestore() + echo "\r" + return (a:0 && answer =~? '^a') ? 2 : (answer =~? '^y') ? 1 : 0 +endfunction + +function! s:ask_no_interrupt(...) + try + return call('s:ask', a:000) + catch + return 0 + endtry +endfunction + +function! s:lazy(plug, opt) + return has_key(a:plug, a:opt) && + \ (empty(s:to_a(a:plug[a:opt])) || + \ !isdirectory(a:plug.dir) || + \ len(s:glob(s:rtp(a:plug), 'plugin')) || + \ len(s:glob(s:rtp(a:plug), 'after/plugin'))) +endfunction + +function! plug#end() + if !exists('g:plugs') + return s:err('plug#end() called without calling plug#begin() first') + endif + + if exists('#PlugLOD') + augroup PlugLOD + autocmd! + augroup END + augroup! PlugLOD + endif + let lod = { 'ft': {}, 'map': {}, 'cmd': {} } + + if get(g:, 'did_load_filetypes', 0) + filetype off + endif + for name in g:plugs_order + if !has_key(g:plugs, name) + continue + endif + let plug = g:plugs[name] + if get(s:loaded, name, 0) || !s:lazy(plug, 'on') && !s:lazy(plug, 'for') + let s:loaded[name] = 1 + continue + endif + + if has_key(plug, 'on') + let s:triggers[name] = { 'map': [], 'cmd': [] } + for cmd in s:to_a(plug.on) + if cmd =~? '^.\+' + if empty(mapcheck(cmd)) && empty(mapcheck(cmd, 'i')) + call s:assoc(lod.map, cmd, name) + endif + call add(s:triggers[name].map, cmd) + elseif cmd =~# '^[A-Z]' + let cmd = substitute(cmd, '!*$', '', '') + if exists(':'.cmd) != 2 + call s:assoc(lod.cmd, cmd, name) + endif + call add(s:triggers[name].cmd, cmd) + else + call s:err('Invalid `on` option: '.cmd. + \ '. Should start with an uppercase letter or ``.') + endif + endfor + endif + + if has_key(plug, 'for') + let types = s:to_a(plug.for) + if !empty(types) + augroup filetypedetect + call s:source(s:rtp(plug), 'ftdetect/**/*.vim', 'after/ftdetect/**/*.vim') + if has('nvim-0.5.0') + call s:source(s:rtp(plug), 'ftdetect/**/*.lua', 'after/ftdetect/**/*.lua') + endif + augroup END + endif + for type in types + call s:assoc(lod.ft, type, name) + endfor + endif + endfor + + for [cmd, names] in items(lod.cmd) + execute printf( + \ has('patch-7.4.1898') + \ ? 'command! -nargs=* -range -bang -complete=file %s call s:lod_cmd(%s, "", , , , ,%s)' + \ : 'command! -nargs=* -range -bang -complete=file %s call s:lod_cmd(%s, "", , , , %s)' + \ , cmd, string(cmd), string(names)) + endfor + + for [map, names] in items(lod.map) + for [mode, map_prefix, key_prefix] in + \ [['i', '', ''], ['n', '', ''], ['v', '', 'gv'], ['o', '', '']] + execute printf( + \ '%snoremap %s %s:call lod_map(%s, %s, %s, "%s")', + \ mode, map, map_prefix, string(map), string(names), mode != 'i', key_prefix) + endfor + endfor + + for [ft, names] in items(lod.ft) + augroup PlugLOD + execute printf('autocmd FileType %s call lod_ft(%s, %s)', + \ ft, string(ft), string(names)) + augroup END + endfor + + call s:reorg_rtp() + filetype plugin indent on + if has('vim_starting') + if has('syntax') && !exists('g:syntax_on') + syntax enable + end + else + call s:reload_plugins() + endif +endfunction + +function! s:loaded_names() + return filter(copy(g:plugs_order), 'get(s:loaded, v:val, 0)') +endfunction + +function! s:load_plugin(spec) + call s:source(s:rtp(a:spec), 'plugin/**/*.vim', 'after/plugin/**/*.vim') + if has('nvim-0.5.0') + call s:source(s:rtp(a:spec), 'plugin/**/*.lua', 'after/plugin/**/*.lua') + endif +endfunction + +function! s:reload_plugins() + for name in s:loaded_names() + call s:load_plugin(g:plugs[name]) + endfor +endfunction + +function! s:trim(str) + return substitute(a:str, '[\/]\+$', '', '') +endfunction + +function! s:version_requirement(val, min) + for idx in range(0, len(a:min) - 1) + let v = get(a:val, idx, 0) + if v < a:min[idx] | return 0 + elseif v > a:min[idx] | return 1 + endif + endfor + return 1 +endfunction + +function! s:git_version_requirement(...) + if !exists('s:git_version') + let s:git_version = map(split(split(s:system(['git', '--version']))[2], '\.'), 'str2nr(v:val)') + endif + return s:version_requirement(s:git_version, a:000) +endfunction + +function! s:progress_opt(base) + return a:base && !s:is_win && + \ s:git_version_requirement(1, 7, 1) ? '--progress' : '' +endfunction + +function! s:rtp(spec) + return s:path(a:spec.dir . get(a:spec, 'rtp', '')) +endfunction + +if s:is_win + function! s:path(path) + return s:trim(substitute(a:path, '/', '\', 'g')) + endfunction + + function! s:dirpath(path) + return s:path(a:path) . '\' + endfunction + + function! s:is_local_plug(repo) + return a:repo =~? '^[a-z]:\|^[%~]' + endfunction + + " Copied from fzf + function! s:wrap_cmds(cmds) + let cmds = [ + \ '@echo off', + \ 'setlocal enabledelayedexpansion'] + \ + (type(a:cmds) == type([]) ? a:cmds : [a:cmds]) + \ + ['endlocal'] + if has('iconv') + if !exists('s:codepage') + let s:codepage = libcallnr('kernel32.dll', 'GetACP', 0) + endif + return map(cmds, printf('iconv(v:val."\r", "%s", "cp%d")', &encoding, s:codepage)) + endif + return map(cmds, 'v:val."\r"') + endfunction + + function! s:batchfile(cmd) + let batchfile = s:plug_tempname().'.bat' + call writefile(s:wrap_cmds(a:cmd), batchfile) + let cmd = plug#shellescape(batchfile, {'shell': &shell, 'script': 0}) + if s:is_powershell(&shell) + let cmd = '& ' . cmd + endif + return [batchfile, cmd] + endfunction +else + function! s:path(path) + return s:trim(a:path) + endfunction + + function! s:dirpath(path) + return substitute(a:path, '[/\\]*$', '/', '') + endfunction + + function! s:is_local_plug(repo) + return a:repo[0] =~ '[/$~]' + endfunction +endif + +function! s:err(msg) + echohl ErrorMsg + echom '[vim-plug] '.a:msg + echohl None +endfunction + +function! s:warn(cmd, msg) + echohl WarningMsg + execute a:cmd 'a:msg' + echohl None +endfunction + +function! s:esc(path) + return escape(a:path, ' ') +endfunction + +function! s:escrtp(path) + return escape(a:path, ' ,') +endfunction + +function! s:remove_rtp() + for name in s:loaded_names() + let rtp = s:rtp(g:plugs[name]) + execute 'set rtp-='.s:escrtp(rtp) + let after = globpath(rtp, 'after') + if isdirectory(after) + execute 'set rtp-='.s:escrtp(after) + endif + endfor +endfunction + +function! s:reorg_rtp() + if !empty(s:first_rtp) + execute 'set rtp-='.s:first_rtp + execute 'set rtp-='.s:last_rtp + endif + + " &rtp is modified from outside + if exists('s:prtp') && s:prtp !=# &rtp + call s:remove_rtp() + unlet! s:middle + endif + + let s:middle = get(s:, 'middle', &rtp) + let rtps = map(s:loaded_names(), 's:rtp(g:plugs[v:val])') + let afters = filter(map(copy(rtps), 'globpath(v:val, "after")'), '!empty(v:val)') + let rtp = join(map(rtps, 'escape(v:val, ",")'), ',') + \ . ','.s:middle.',' + \ . join(map(afters, 'escape(v:val, ",")'), ',') + let &rtp = substitute(substitute(rtp, ',,*', ',', 'g'), '^,\|,$', '', 'g') + let s:prtp = &rtp + + if !empty(s:first_rtp) + execute 'set rtp^='.s:first_rtp + execute 'set rtp+='.s:last_rtp + endif +endfunction + +function! s:doautocmd(...) + if exists('#'.join(a:000, '#')) + execute 'doautocmd' ((v:version > 703 || has('patch442')) ? '' : '') join(a:000) + endif +endfunction + +function! s:dobufread(names) + for name in a:names + let path = s:rtp(g:plugs[name]) + for dir in ['ftdetect', 'ftplugin', 'after/ftdetect', 'after/ftplugin'] + if len(finddir(dir, path)) + if exists('#BufRead') + doautocmd BufRead + endif + return + endif + endfor + endfor +endfunction + +function! plug#load(...) + if a:0 == 0 + return s:err('Argument missing: plugin name(s) required') + endif + if !exists('g:plugs') + return s:err('plug#begin was not called') + endif + let names = a:0 == 1 && type(a:1) == s:TYPE.list ? a:1 : a:000 + let unknowns = filter(copy(names), '!has_key(g:plugs, v:val)') + if !empty(unknowns) + let s = len(unknowns) > 1 ? 's' : '' + return s:err(printf('Unknown plugin%s: %s', s, join(unknowns, ', '))) + end + let unloaded = filter(copy(names), '!get(s:loaded, v:val, 0)') + if !empty(unloaded) + for name in unloaded + call s:lod([name], ['ftdetect', 'after/ftdetect', 'plugin', 'after/plugin']) + endfor + call s:dobufread(unloaded) + return 1 + end + return 0 +endfunction + +function! s:remove_triggers(name) + if !has_key(s:triggers, a:name) + return + endif + for cmd in s:triggers[a:name].cmd + execute 'silent! delc' cmd + endfor + for map in s:triggers[a:name].map + execute 'silent! unmap' map + execute 'silent! iunmap' map + endfor + call remove(s:triggers, a:name) +endfunction + +function! s:lod(names, types, ...) + for name in a:names + call s:remove_triggers(name) + let s:loaded[name] = 1 + endfor + call s:reorg_rtp() + + for name in a:names + let rtp = s:rtp(g:plugs[name]) + for dir in a:types + call s:source(rtp, dir.'/**/*.vim') + if has('nvim-0.5.0') " see neovim#14686 + call s:source(rtp, dir.'/**/*.lua') + endif + endfor + if a:0 + if !s:source(rtp, a:1) && !empty(s:glob(rtp, a:2)) + execute 'runtime' a:1 + endif + call s:source(rtp, a:2) + endif + call s:doautocmd('User', name) + endfor +endfunction + +function! s:lod_ft(pat, names) + let syn = 'syntax/'.a:pat.'.vim' + call s:lod(a:names, ['plugin', 'after/plugin'], syn, 'after/'.syn) + execute 'autocmd! PlugLOD FileType' a:pat + call s:doautocmd('filetypeplugin', 'FileType') + call s:doautocmd('filetypeindent', 'FileType') +endfunction + +if has('patch-7.4.1898') + function! s:lod_cmd(cmd, bang, l1, l2, args, mods, names) + call s:lod(a:names, ['ftdetect', 'after/ftdetect', 'plugin', 'after/plugin']) + call s:dobufread(a:names) + execute printf('%s %s%s%s %s', a:mods, (a:l1 == a:l2 ? '' : (a:l1.','.a:l2)), a:cmd, a:bang, a:args) + endfunction +else + function! s:lod_cmd(cmd, bang, l1, l2, args, names) + call s:lod(a:names, ['ftdetect', 'after/ftdetect', 'plugin', 'after/plugin']) + call s:dobufread(a:names) + execute printf('%s%s%s %s', (a:l1 == a:l2 ? '' : (a:l1.','.a:l2)), a:cmd, a:bang, a:args) + endfunction +endif + +function! s:lod_map(map, names, with_prefix, prefix) + call s:lod(a:names, ['ftdetect', 'after/ftdetect', 'plugin', 'after/plugin']) + call s:dobufread(a:names) + let extra = '' + while 1 + let c = getchar(0) + if c == 0 + break + endif + let extra .= nr2char(c) + endwhile + + if a:with_prefix + let prefix = v:count ? v:count : '' + let prefix .= '"'.v:register.a:prefix + if mode(1) == 'no' + if v:operator == 'c' + let prefix = "\" . prefix + endif + let prefix .= v:operator + endif + call feedkeys(prefix, 'n') + endif + call feedkeys(substitute(a:map, '^', "\", '') . extra) +endfunction + +function! plug#(repo, ...) + if a:0 > 1 + return s:err('Invalid number of arguments (1..2)') + endif + + try + let repo = s:trim(a:repo) + let opts = a:0 == 1 ? s:parse_options(a:1) : s:base_spec + let name = get(opts, 'as', s:plug_fnamemodify(repo, ':t:s?\.git$??')) + let spec = extend(s:infer_properties(name, repo), opts) + if !has_key(g:plugs, name) + call add(g:plugs_order, name) + endif + let g:plugs[name] = spec + let s:loaded[name] = get(s:loaded, name, 0) + catch + return s:err(repo . ' ' . v:exception) + endtry +endfunction + +function! s:parse_options(arg) + let opts = copy(s:base_spec) + let type = type(a:arg) + let opt_errfmt = 'Invalid argument for "%s" option of :Plug (expected: %s)' + if type == s:TYPE.string + if empty(a:arg) + throw printf(opt_errfmt, 'tag', 'string') + endif + let opts.tag = a:arg + elseif type == s:TYPE.dict + for opt in ['branch', 'tag', 'commit', 'rtp', 'dir', 'as'] + if has_key(a:arg, opt) + \ && (type(a:arg[opt]) != s:TYPE.string || empty(a:arg[opt])) + throw printf(opt_errfmt, opt, 'string') + endif + endfor + for opt in ['on', 'for'] + if has_key(a:arg, opt) + \ && type(a:arg[opt]) != s:TYPE.list + \ && (type(a:arg[opt]) != s:TYPE.string || empty(a:arg[opt])) + throw printf(opt_errfmt, opt, 'string or list') + endif + endfor + if has_key(a:arg, 'do') + \ && type(a:arg.do) != s:TYPE.funcref + \ && (type(a:arg.do) != s:TYPE.string || empty(a:arg.do)) + throw printf(opt_errfmt, 'do', 'string or funcref') + endif + call extend(opts, a:arg) + if has_key(opts, 'dir') + let opts.dir = s:dirpath(s:plug_expand(opts.dir)) + endif + else + throw 'Invalid argument type (expected: string or dictionary)' + endif + return opts +endfunction + +function! s:infer_properties(name, repo) + let repo = a:repo + if s:is_local_plug(repo) + return { 'dir': s:dirpath(s:plug_expand(repo)) } + else + if repo =~ ':' + let uri = repo + else + if repo !~ '/' + throw printf('Invalid argument: %s (implicit `vim-scripts'' expansion is deprecated)', repo) + endif + let fmt = get(g:, 'plug_url_format', 'https://git::@github.com/%s.git') + let uri = printf(fmt, repo) + endif + return { 'dir': s:dirpath(g:plug_home.'/'.a:name), 'uri': uri } + endif +endfunction + +function! s:install(force, names) + call s:update_impl(0, a:force, a:names) +endfunction + +function! s:update(force, names) + call s:update_impl(1, a:force, a:names) +endfunction + +function! plug#helptags() + if !exists('g:plugs') + return s:err('plug#begin was not called') + endif + for spec in values(g:plugs) + let docd = join([s:rtp(spec), 'doc'], '/') + if isdirectory(docd) + silent! execute 'helptags' s:esc(docd) + endif + endfor + return 1 +endfunction + +function! s:syntax() + syntax clear + syntax region plug1 start=/\%1l/ end=/\%2l/ contains=plugNumber + syntax region plug2 start=/\%2l/ end=/\%3l/ contains=plugBracket,plugX,plugAbort + syn match plugNumber /[0-9]\+[0-9.]*/ contained + syn match plugBracket /[[\]]/ contained + syn match plugX /x/ contained + syn match plugAbort /\~/ contained + syn match plugDash /^-\{1}\ / + syn match plugPlus /^+/ + syn match plugStar /^*/ + syn match plugMessage /\(^- \)\@<=.*/ + syn match plugName /\(^- \)\@<=[^ ]*:/ + syn match plugSha /\%(: \)\@<=[0-9a-f]\{4,}$/ + syn match plugTag /(tag: [^)]\+)/ + syn match plugInstall /\(^+ \)\@<=[^:]*/ + syn match plugUpdate /\(^* \)\@<=[^:]*/ + syn match plugCommit /^ \X*[0-9a-f]\{7,9} .*/ contains=plugRelDate,plugEdge,plugTag + syn match plugEdge /^ \X\+$/ + syn match plugEdge /^ \X*/ contained nextgroup=plugSha + syn match plugSha /[0-9a-f]\{7,9}/ contained + syn match plugRelDate /([^)]*)$/ contained + syn match plugNotLoaded /(not loaded)$/ + syn match plugError /^x.*/ + syn region plugDeleted start=/^\~ .*/ end=/^\ze\S/ + syn match plugH2 /^.*:\n-\+$/ + syn match plugH2 /^-\{2,}/ + syn keyword Function PlugInstall PlugStatus PlugUpdate PlugClean + hi def link plug1 Title + hi def link plug2 Repeat + hi def link plugH2 Type + hi def link plugX Exception + hi def link plugAbort Ignore + hi def link plugBracket Structure + hi def link plugNumber Number + + hi def link plugDash Special + hi def link plugPlus Constant + hi def link plugStar Boolean + + hi def link plugMessage Function + hi def link plugName Label + hi def link plugInstall Function + hi def link plugUpdate Type + + hi def link plugError Error + hi def link plugDeleted Ignore + hi def link plugRelDate Comment + hi def link plugEdge PreProc + hi def link plugSha Identifier + hi def link plugTag Constant + + hi def link plugNotLoaded Comment +endfunction + +function! s:lpad(str, len) + return a:str . repeat(' ', a:len - len(a:str)) +endfunction + +function! s:lines(msg) + return split(a:msg, "[\r\n]") +endfunction + +function! s:lastline(msg) + return get(s:lines(a:msg), -1, '') +endfunction + +function! s:new_window() + execute get(g:, 'plug_window', '-tabnew') +endfunction + +function! s:plug_window_exists() + let buflist = tabpagebuflist(s:plug_tab) + return !empty(buflist) && index(buflist, s:plug_buf) >= 0 +endfunction + +function! s:switch_in() + if !s:plug_window_exists() + return 0 + endif + + if winbufnr(0) != s:plug_buf + let s:pos = [tabpagenr(), winnr(), winsaveview()] + execute 'normal!' s:plug_tab.'gt' + let winnr = bufwinnr(s:plug_buf) + execute winnr.'wincmd w' + call add(s:pos, winsaveview()) + else + let s:pos = [winsaveview()] + endif + + setlocal modifiable + return 1 +endfunction + +function! s:switch_out(...) + call winrestview(s:pos[-1]) + setlocal nomodifiable + if a:0 > 0 + execute a:1 + endif + + if len(s:pos) > 1 + execute 'normal!' s:pos[0].'gt' + execute s:pos[1] 'wincmd w' + call winrestview(s:pos[2]) + endif +endfunction + +function! s:finish_bindings() + nnoremap R :call retry() + nnoremap D :PlugDiff + nnoremap S :PlugStatus + nnoremap U :call status_update() + xnoremap U :call status_update() + nnoremap ]] :silent! call section('') + nnoremap [[ :silent! call section('b') +endfunction + +function! s:prepare(...) + if empty(s:plug_getcwd()) + throw 'Invalid current working directory. Cannot proceed.' + endif + + for evar in ['$GIT_DIR', '$GIT_WORK_TREE'] + if exists(evar) + throw evar.' detected. Cannot proceed.' + endif + endfor + + call s:job_abort(0) + if s:switch_in() + if b:plug_preview == 1 + pc + endif + enew + else + call s:new_window() + endif + + nnoremap q :call close_pane() + if a:0 == 0 + call s:finish_bindings() + endif + let b:plug_preview = -1 + let s:plug_tab = tabpagenr() + let s:plug_buf = winbufnr(0) + call s:assign_name() + + for k in ['', 'L', 'o', 'X', 'd', 'dd'] + execute 'silent! unmap ' k + endfor + setlocal buftype=nofile bufhidden=wipe nobuflisted nolist noswapfile nowrap cursorline modifiable nospell + if exists('+colorcolumn') + setlocal colorcolumn= + endif + setf vim-plug + if exists('g:syntax_on') + call s:syntax() + endif +endfunction + +function! s:close_pane() + if b:plug_preview == 1 + pc + let b:plug_preview = -1 + elseif exists('s:jobs') && !empty(s:jobs) + call s:job_abort(1) + else + bd + endif +endfunction + +function! s:assign_name() + " Assign buffer name + let prefix = '[Plugins]' + let name = prefix + let idx = 2 + while bufexists(name) + let name = printf('%s (%s)', prefix, idx) + let idx = idx + 1 + endwhile + silent! execute 'f' fnameescape(name) +endfunction + +function! s:chsh(swap) + let prev = [&shell, &shellcmdflag, &shellredir] + if !s:is_win + set shell=sh + endif + if a:swap + if s:is_powershell(&shell) + let &shellredir = '2>&1 | Out-File -Encoding UTF8 %s' + elseif &shell =~# 'sh' || &shell =~# 'cmd\(\.exe\)\?$' + set shellredir=>%s\ 2>&1 + endif + endif + return prev +endfunction + +function! s:bang(cmd, ...) + let batchfile = '' + try + let [sh, shellcmdflag, shrd] = s:chsh(a:0) + " FIXME: Escaping is incomplete. We could use shellescape with eval, + " but it won't work on Windows. + let cmd = a:0 ? s:with_cd(a:cmd, a:1) : a:cmd + if s:is_win + let [batchfile, cmd] = s:batchfile(cmd) + endif + let g:_plug_bang = (s:is_win && has('gui_running') ? 'silent ' : '').'!'.escape(cmd, '#!%') + execute "normal! :execute g:_plug_bang\\" + finally + unlet g:_plug_bang + let [&shell, &shellcmdflag, &shellredir] = [sh, shellcmdflag, shrd] + if s:is_win && filereadable(batchfile) + call delete(batchfile) + endif + endtry + return v:shell_error ? 'Exit status: ' . v:shell_error : '' +endfunction + +function! s:regress_bar() + let bar = substitute(getline(2)[1:-2], '.*\zs=', 'x', '') + call s:progress_bar(2, bar, len(bar)) +endfunction + +function! s:is_updated(dir) + return !empty(s:system_chomp(['git', 'log', '--pretty=format:%h', 'HEAD...HEAD@{1}'], a:dir)) +endfunction + +function! s:do(pull, force, todo) + if has('nvim') + " Reset &rtp to invalidate Neovim cache of loaded Lua modules + " See https://github.com/junegunn/vim-plug/pull/1157#issuecomment-1809226110 + let &rtp = &rtp + endif + for [name, spec] in items(a:todo) + if !isdirectory(spec.dir) + continue + endif + let installed = has_key(s:update.new, name) + let updated = installed ? 0 : + \ (a:pull && index(s:update.errors, name) < 0 && s:is_updated(spec.dir)) + if a:force || installed || updated + execute 'cd' s:esc(spec.dir) + call append(3, '- Post-update hook for '. name .' ... ') + let error = '' + let type = type(spec.do) + if type == s:TYPE.string + if spec.do[0] == ':' + if !get(s:loaded, name, 0) + let s:loaded[name] = 1 + call s:reorg_rtp() + endif + call s:load_plugin(spec) + try + execute spec.do[1:] + catch + let error = v:exception + endtry + if !s:plug_window_exists() + cd - + throw 'Warning: vim-plug was terminated by the post-update hook of '.name + endif + else + let error = s:bang(spec.do) + endif + elseif type == s:TYPE.funcref + try + call s:load_plugin(spec) + let status = installed ? 'installed' : (updated ? 'updated' : 'unchanged') + call spec.do({ 'name': name, 'status': status, 'force': a:force }) + catch + let error = v:exception + endtry + else + let error = 'Invalid hook type' + endif + call s:switch_in() + call setline(4, empty(error) ? (getline(4) . 'OK') + \ : ('x' . getline(4)[1:] . error)) + if !empty(error) + call add(s:update.errors, name) + call s:regress_bar() + endif + cd - + endif + endfor +endfunction + +function! s:hash_match(a, b) + return stridx(a:a, a:b) == 0 || stridx(a:b, a:a) == 0 +endfunction + +function! s:disable_credential_helper() + return s:git_version_requirement(2) && get(g:, 'plug_disable_credential_helper', 1) +endfunction + +function! s:checkout(spec) + let sha = a:spec.commit + let output = s:git_revision(a:spec.dir) + let error = 0 + if !empty(output) && !s:hash_match(sha, s:lines(output)[0]) + let credential_helper = s:disable_credential_helper() ? '-c credential.helper= ' : '' + let output = s:system( + \ 'git '.credential_helper.'fetch --depth 999999 && git checkout '.plug#shellescape(sha).' --', a:spec.dir) + let error = s:shell_error + endif + return [output, error] +endfunction + +function! s:finish(pull) + let new_frozen = len(filter(keys(s:update.new), 'g:plugs[v:val].frozen')) + if new_frozen + let s = new_frozen > 1 ? 's' : '' + call append(3, printf('- Installed %d frozen plugin%s', new_frozen, s)) + endif + call append(3, '- Finishing ... ') | 4 + redraw + call plug#helptags() + call plug#end() + call setline(4, getline(4) . 'Done!') + redraw + let msgs = [] + if !empty(s:update.errors) + call add(msgs, "Press 'R' to retry.") + endif + if a:pull && len(s:update.new) < len(filter(getline(5, '$'), + \ "v:val =~ '^- ' && v:val !~# 'Already up.to.date'")) + call add(msgs, "Press 'D' to see the updated changes.") + endif + echo join(msgs, ' ') + call s:finish_bindings() +endfunction + +function! s:retry() + if empty(s:update.errors) + return + endif + echo + call s:update_impl(s:update.pull, s:update.force, + \ extend(copy(s:update.errors), [s:update.threads])) +endfunction + +function! s:is_managed(name) + return has_key(g:plugs[a:name], 'uri') +endfunction + +function! s:names(...) + return sort(filter(keys(g:plugs), 'stridx(v:val, a:1) == 0 && s:is_managed(v:val)')) +endfunction + +function! s:check_ruby() + silent! ruby require 'thread'; VIM::command("let g:plug_ruby = '#{RUBY_VERSION}'") + if !exists('g:plug_ruby') + redraw! + return s:warn('echom', 'Warning: Ruby interface is broken') + endif + let ruby_version = split(g:plug_ruby, '\.') + unlet g:plug_ruby + return s:version_requirement(ruby_version, [1, 8, 7]) +endfunction + +function! s:update_impl(pull, force, args) abort + let sync = index(a:args, '--sync') >= 0 || has('vim_starting') + let args = filter(copy(a:args), 'v:val != "--sync"') + let threads = (len(args) > 0 && args[-1] =~ '^[1-9][0-9]*$') ? + \ remove(args, -1) : get(g:, 'plug_threads', 16) + + let managed = filter(deepcopy(g:plugs), 's:is_managed(v:key)') + let todo = empty(args) ? filter(managed, '!v:val.frozen || !isdirectory(v:val.dir)') : + \ filter(managed, 'index(args, v:key) >= 0') + + if empty(todo) + return s:warn('echo', 'No plugin to '. (a:pull ? 'update' : 'install')) + endif + + if !s:is_win && s:git_version_requirement(2, 3) + let s:git_terminal_prompt = exists('$GIT_TERMINAL_PROMPT') ? $GIT_TERMINAL_PROMPT : '' + let $GIT_TERMINAL_PROMPT = 0 + for plug in values(todo) + let plug.uri = substitute(plug.uri, + \ '^https://git::@github\.com', 'https://github.com', '') + endfor + endif + + if !isdirectory(g:plug_home) + try + call mkdir(g:plug_home, 'p') + catch + return s:err(printf('Invalid plug directory: %s. '. + \ 'Try to call plug#begin with a valid directory', g:plug_home)) + endtry + endif + + if has('nvim') && !exists('*jobwait') && threads > 1 + call s:warn('echom', '[vim-plug] Update Neovim for parallel installer') + endif + + let use_job = s:nvim || s:vim8 + let python = (has('python') || has('python3')) && !use_job + let ruby = has('ruby') && !use_job && (v:version >= 703 || v:version == 702 && has('patch374')) && !(s:is_win && has('gui_running')) && threads > 1 && s:check_ruby() + + let s:update = { + \ 'start': reltime(), + \ 'all': todo, + \ 'todo': copy(todo), + \ 'errors': [], + \ 'pull': a:pull, + \ 'force': a:force, + \ 'new': {}, + \ 'threads': (python || ruby || use_job) ? min([len(todo), threads]) : 1, + \ 'bar': '', + \ 'fin': 0 + \ } + + call s:prepare(1) + call append(0, ['', '']) + normal! 2G + silent! redraw + + " Set remote name, overriding a possible user git config's clone.defaultRemoteName + let s:clone_opt = ['--origin', 'origin'] + if get(g:, 'plug_shallow', 1) + call extend(s:clone_opt, ['--depth', '1']) + if s:git_version_requirement(1, 7, 10) + call add(s:clone_opt, '--no-single-branch') + endif + endif + + if has('win32unix') || has('wsl') + call extend(s:clone_opt, ['-c', 'core.eol=lf', '-c', 'core.autocrlf=input']) + endif + + let s:submodule_opt = s:git_version_requirement(2, 8) ? ' --jobs='.threads : '' + + " Python version requirement (>= 2.7) + if python && !has('python3') && !ruby && !use_job && s:update.threads > 1 + redir => pyv + silent python import platform; print platform.python_version() + redir END + let python = s:version_requirement( + \ map(split(split(pyv)[0], '\.'), 'str2nr(v:val)'), [2, 6]) + endif + + if (python || ruby) && s:update.threads > 1 + try + let imd = &imd + if s:mac_gui + set noimd + endif + if ruby + call s:update_ruby() + else + call s:update_python() + endif + catch + let lines = getline(4, '$') + let printed = {} + silent! 4,$d _ + for line in lines + let name = s:extract_name(line, '.', '') + if empty(name) || !has_key(printed, name) + call append('$', line) + if !empty(name) + let printed[name] = 1 + if line[0] == 'x' && index(s:update.errors, name) < 0 + call add(s:update.errors, name) + end + endif + endif + endfor + finally + let &imd = imd + call s:update_finish() + endtry + else + call s:update_vim() + while use_job && sync + sleep 100m + if s:update.fin + break + endif + endwhile + endif +endfunction + +function! s:log4(name, msg) + call setline(4, printf('- %s (%s)', a:msg, a:name)) + redraw +endfunction + +function! s:update_finish() + if exists('s:git_terminal_prompt') + let $GIT_TERMINAL_PROMPT = s:git_terminal_prompt + endif + if s:switch_in() + call append(3, '- Updating ...') | 4 + for [name, spec] in items(filter(copy(s:update.all), 'index(s:update.errors, v:key) < 0 && (s:update.force || s:update.pull || has_key(s:update.new, v:key))')) + let [pos, _] = s:logpos(name) + if !pos + continue + endif + let out = '' + let error = 0 + if has_key(spec, 'commit') + call s:log4(name, 'Checking out '.spec.commit) + let [out, error] = s:checkout(spec) + elseif has_key(spec, 'tag') + let tag = spec.tag + if tag =~ '\*' + let tags = s:lines(s:system('git tag --list '.plug#shellescape(tag).' --sort -version:refname 2>&1', spec.dir)) + if !s:shell_error && !empty(tags) + let tag = tags[0] + call s:log4(name, printf('Latest tag for %s -> %s', spec.tag, tag)) + call append(3, '') + endif + endif + call s:log4(name, 'Checking out '.tag) + let out = s:system('git checkout -q '.plug#shellescape(tag).' -- 2>&1', spec.dir) + let error = s:shell_error + endif + if !error && filereadable(spec.dir.'/.gitmodules') && + \ (s:update.force || has_key(s:update.new, name) || s:is_updated(spec.dir)) + call s:log4(name, 'Updating submodules. This may take a while.') + let out .= s:bang('git submodule update --init --recursive'.s:submodule_opt.' 2>&1', spec.dir) + let error = v:shell_error + endif + let msg = s:format_message(error ? 'x': '-', name, out) + if error + call add(s:update.errors, name) + call s:regress_bar() + silent execute pos 'd _' + call append(4, msg) | 4 + elseif !empty(out) + call setline(pos, msg[0]) + endif + redraw + endfor + silent 4 d _ + try + call s:do(s:update.pull, s:update.force, filter(copy(s:update.all), 'index(s:update.errors, v:key) < 0 && has_key(v:val, "do")')) + catch + call s:warn('echom', v:exception) + call s:warn('echo', '') + return + endtry + call s:finish(s:update.pull) + call setline(1, 'Updated. Elapsed time: ' . split(reltimestr(reltime(s:update.start)))[0] . ' sec.') + call s:switch_out('normal! gg') + endif +endfunction + +function! s:mark_aborted(name, message) + let attrs = { 'running': 0, 'error': 1, 'abort': 1, 'lines': [a:message] } + let s:jobs[a:name] = extend(get(s:jobs, a:name, {}), attrs) +endfunction + +function! s:job_abort(cancel) + if (!s:nvim && !s:vim8) || !exists('s:jobs') + return + endif + + for [name, j] in items(s:jobs) + if s:nvim + silent! call jobstop(j.jobid) + elseif s:vim8 + silent! call job_stop(j.jobid) + endif + if j.new + call s:rm_rf(g:plugs[name].dir) + endif + if a:cancel + call s:mark_aborted(name, 'Aborted') + endif + endfor + + if a:cancel + for todo in values(s:update.todo) + let todo.abort = 1 + endfor + else + let s:jobs = {} + endif +endfunction + +function! s:last_non_empty_line(lines) + let len = len(a:lines) + for idx in range(len) + let line = a:lines[len-idx-1] + if !empty(line) + return line + endif + endfor + return '' +endfunction + +function! s:bullet_for(job, ...) + if a:job.running + return a:job.new ? '+' : '*' + endif + if get(a:job, 'abort', 0) + return '~' + endif + return a:job.error ? 'x' : get(a:000, 0, '-') +endfunction + +function! s:job_out_cb(self, data) abort + let self = a:self + let data = remove(self.lines, -1) . a:data + let lines = map(split(data, "\n", 1), 'split(v:val, "\r", 1)[-1]') + call extend(self.lines, lines) + " To reduce the number of buffer updates + let self.tick = get(self, 'tick', -1) + 1 + if !self.running || self.tick % len(s:jobs) == 0 + let result = self.error ? join(self.lines, "\n") : s:last_non_empty_line(self.lines) + if len(result) + call s:log(s:bullet_for(self), self.name, result) + endif + endif +endfunction + +function! s:job_exit_cb(self, data) abort + let a:self.running = 0 + let a:self.error = a:data != 0 + call s:reap(a:self.name) + call s:tick() +endfunction + +function! s:job_cb(fn, job, ch, data) + if !s:plug_window_exists() " plug window closed + return s:job_abort(0) + endif + call call(a:fn, [a:job, a:data]) +endfunction + +function! s:nvim_cb(job_id, data, event) dict abort + return (a:event == 'stdout' || a:event == 'stderr') ? + \ s:job_cb('s:job_out_cb', self, 0, join(a:data, "\n")) : + \ s:job_cb('s:job_exit_cb', self, 0, a:data) +endfunction + +function! s:spawn(name, spec, queue, opts) + let job = { 'name': a:name, 'spec': a:spec, 'running': 1, 'error': 0, 'lines': [''], + \ 'new': get(a:opts, 'new', 0), 'queue': copy(a:queue) } + let Item = remove(job.queue, 0) + let argv = type(Item) == s:TYPE.funcref ? call(Item, [a:spec]) : Item + let s:jobs[a:name] = job + + if s:nvim + if has_key(a:opts, 'dir') + let job.cwd = a:opts.dir + endif + call extend(job, { + \ 'on_stdout': function('s:nvim_cb'), + \ 'on_stderr': function('s:nvim_cb'), + \ 'on_exit': function('s:nvim_cb'), + \ }) + let jid = s:plug_call('jobstart', argv, job) + if jid > 0 + let job.jobid = jid + else + let job.running = 0 + let job.error = 1 + let job.lines = [jid < 0 ? argv[0].' is not executable' : + \ 'Invalid arguments (or job table is full)'] + endif + elseif s:vim8 + let cmd = join(map(copy(argv), 'plug#shellescape(v:val, {"script": 0})')) + if has_key(a:opts, 'dir') + let cmd = s:with_cd(cmd, a:opts.dir, 0) + endif + let argv = s:is_win ? ['cmd', '/s', '/c', '"'.cmd.'"'] : ['sh', '-c', cmd] + let jid = job_start(s:is_win ? join(argv, ' ') : argv, { + \ 'out_cb': function('s:job_cb', ['s:job_out_cb', job]), + \ 'err_cb': function('s:job_cb', ['s:job_out_cb', job]), + \ 'exit_cb': function('s:job_cb', ['s:job_exit_cb', job]), + \ 'err_mode': 'raw', + \ 'out_mode': 'raw' + \}) + if job_status(jid) == 'run' + let job.jobid = jid + else + let job.running = 0 + let job.error = 1 + let job.lines = ['Failed to start job'] + endif + else + let job.lines = s:lines(call('s:system', has_key(a:opts, 'dir') ? [argv, a:opts.dir] : [argv])) + let job.error = s:shell_error != 0 + let job.running = 0 + endif +endfunction + +function! s:reap(name) + let job = remove(s:jobs, a:name) + if job.error + call add(s:update.errors, a:name) + elseif get(job, 'new', 0) + let s:update.new[a:name] = 1 + endif + + let more = len(get(job, 'queue', [])) + let result = job.error ? join(job.lines, "\n") : s:last_non_empty_line(job.lines) + if len(result) + call s:log(s:bullet_for(job), a:name, result) + endif + + if !job.error && more + let job.spec.queue = job.queue + let s:update.todo[a:name] = job.spec + else + let s:update.bar .= s:bullet_for(job, '=') + call s:bar() + endif +endfunction + +function! s:bar() + if s:switch_in() + let total = len(s:update.all) + call setline(1, (s:update.pull ? 'Updating' : 'Installing'). + \ ' plugins ('.len(s:update.bar).'/'.total.')') + call s:progress_bar(2, s:update.bar, total) + call s:switch_out() + endif +endfunction + +function! s:logpos(name) + let max = line('$') + for i in range(4, max > 4 ? max : 4) + if getline(i) =~# '^[-+x*] '.a:name.':' + for j in range(i + 1, max > 5 ? max : 5) + if getline(j) !~ '^ ' + return [i, j - 1] + endif + endfor + return [i, i] + endif + endfor + return [0, 0] +endfunction + +function! s:log(bullet, name, lines) + if s:switch_in() + let [b, e] = s:logpos(a:name) + if b > 0 + silent execute printf('%d,%d d _', b, e) + if b > winheight('.') + let b = 4 + endif + else + let b = 4 + endif + " FIXME For some reason, nomodifiable is set after :d in vim8 + setlocal modifiable + call append(b - 1, s:format_message(a:bullet, a:name, a:lines)) + call s:switch_out() + endif +endfunction + +function! s:update_vim() + let s:jobs = {} + + call s:bar() + call s:tick() +endfunction + +function! s:checkout_command(spec) + let a:spec.branch = s:git_origin_branch(a:spec) + return ['git', 'checkout', '-q', a:spec.branch, '--'] +endfunction + +function! s:merge_command(spec) + let a:spec.branch = s:git_origin_branch(a:spec) + return ['git', 'merge', '--ff-only', 'origin/'.a:spec.branch] +endfunction + +function! s:tick() + let pull = s:update.pull + let prog = s:progress_opt(s:nvim || s:vim8) +while 1 " Without TCO, Vim stack is bound to explode + if empty(s:update.todo) + if empty(s:jobs) && !s:update.fin + call s:update_finish() + let s:update.fin = 1 + endif + return + endif + + let name = keys(s:update.todo)[0] + let spec = remove(s:update.todo, name) + if get(spec, 'abort', 0) + call s:mark_aborted(name, 'Skipped') + call s:reap(name) + continue + endif + + let queue = get(spec, 'queue', []) + let new = empty(globpath(spec.dir, '.git', 1)) + + if empty(queue) + call s:log(new ? '+' : '*', name, pull ? 'Updating ...' : 'Installing ...') + redraw + endif + + let has_tag = has_key(spec, 'tag') + if len(queue) + call s:spawn(name, spec, queue, { 'dir': spec.dir }) + elseif !new + let [error, _] = s:git_validate(spec, 0) + if empty(error) + if pull + let cmd = s:disable_credential_helper() ? ['git', '-c', 'credential.helper=', 'fetch'] : ['git', 'fetch'] + if has_tag && !empty(globpath(spec.dir, '.git/shallow')) + call extend(cmd, ['--depth', '99999999']) + endif + if !empty(prog) + call add(cmd, prog) + endif + let queue = [cmd, split('git remote set-head origin -a')] + if !has_tag && !has_key(spec, 'commit') + call extend(queue, [function('s:checkout_command'), function('s:merge_command')]) + endif + call s:spawn(name, spec, queue, { 'dir': spec.dir }) + else + let s:jobs[name] = { 'running': 0, 'lines': ['Already installed'], 'error': 0 } + endif + else + let s:jobs[name] = { 'running': 0, 'lines': s:lines(error), 'error': 1 } + endif + else + let cmd = ['git', 'clone'] + if !has_tag + call extend(cmd, s:clone_opt) + endif + if !empty(prog) + call add(cmd, prog) + endif + call s:spawn(name, spec, [extend(cmd, [spec.uri, s:trim(spec.dir)]), function('s:checkout_command'), function('s:merge_command')], { 'new': 1 }) + endif + + if !s:jobs[name].running + call s:reap(name) + endif + if len(s:jobs) >= s:update.threads + break + endif +endwhile +endfunction + +function! s:update_python() +let py_exe = has('python') ? 'python' : 'python3' +execute py_exe "<< EOF" +import datetime +import functools +import os +try: + import queue +except ImportError: + import Queue as queue +import random +import re +import shutil +import signal +import subprocess +import tempfile +import threading as thr +import time +import traceback +import vim + +G_NVIM = vim.eval("has('nvim')") == '1' +G_PULL = vim.eval('s:update.pull') == '1' +G_RETRIES = int(vim.eval('get(g:, "plug_retries", 2)')) + 1 +G_TIMEOUT = int(vim.eval('get(g:, "plug_timeout", 60)')) +G_CLONE_OPT = ' '.join(vim.eval('s:clone_opt')) +G_PROGRESS = vim.eval('s:progress_opt(1)') +G_LOG_PROB = 1.0 / int(vim.eval('s:update.threads')) +G_STOP = thr.Event() +G_IS_WIN = vim.eval('s:is_win') == '1' + +class PlugError(Exception): + def __init__(self, msg): + self.msg = msg +class CmdTimedOut(PlugError): + pass +class CmdFailed(PlugError): + pass +class InvalidURI(PlugError): + pass +class Action(object): + INSTALL, UPDATE, ERROR, DONE = ['+', '*', 'x', '-'] + +class Buffer(object): + def __init__(self, lock, num_plugs, is_pull): + self.bar = '' + self.event = 'Updating' if is_pull else 'Installing' + self.lock = lock + self.maxy = int(vim.eval('winheight(".")')) + self.num_plugs = num_plugs + + def __where(self, name): + """ Find first line with name in current buffer. Return line num. """ + found, lnum = False, 0 + matcher = re.compile('^[-+x*] {0}:'.format(name)) + for line in vim.current.buffer: + if matcher.search(line) is not None: + found = True + break + lnum += 1 + + if not found: + lnum = -1 + return lnum + + def header(self): + curbuf = vim.current.buffer + curbuf[0] = self.event + ' plugins ({0}/{1})'.format(len(self.bar), self.num_plugs) + + num_spaces = self.num_plugs - len(self.bar) + curbuf[1] = '[{0}{1}]'.format(self.bar, num_spaces * ' ') + + with self.lock: + vim.command('normal! 2G') + vim.command('redraw') + + def write(self, action, name, lines): + first, rest = lines[0], lines[1:] + msg = ['{0} {1}{2}{3}'.format(action, name, ': ' if first else '', first)] + msg.extend([' ' + line for line in rest]) + + try: + if action == Action.ERROR: + self.bar += 'x' + vim.command("call add(s:update.errors, '{0}')".format(name)) + elif action == Action.DONE: + self.bar += '=' + + curbuf = vim.current.buffer + lnum = self.__where(name) + if lnum != -1: # Found matching line num + del curbuf[lnum] + if lnum > self.maxy and action in set([Action.INSTALL, Action.UPDATE]): + lnum = 3 + else: + lnum = 3 + curbuf.append(msg, lnum) + + self.header() + except vim.error: + pass + +class Command(object): + CD = 'cd /d' if G_IS_WIN else 'cd' + + def __init__(self, cmd, cmd_dir=None, timeout=60, cb=None, clean=None): + self.cmd = cmd + if cmd_dir: + self.cmd = '{0} {1} && {2}'.format(Command.CD, cmd_dir, self.cmd) + self.timeout = timeout + self.callback = cb if cb else (lambda msg: None) + self.clean = clean if clean else (lambda: None) + self.proc = None + + @property + def alive(self): + """ Returns true only if command still running. """ + return self.proc and self.proc.poll() is None + + def execute(self, ntries=3): + """ Execute the command with ntries if CmdTimedOut. + Returns the output of the command if no Exception. + """ + attempt, finished, limit = 0, False, self.timeout + + while not finished: + try: + attempt += 1 + result = self.try_command() + finished = True + return result + except CmdTimedOut: + if attempt != ntries: + self.notify_retry() + self.timeout += limit + else: + raise + + def notify_retry(self): + """ Retry required for command, notify user. """ + for count in range(3, 0, -1): + if G_STOP.is_set(): + raise KeyboardInterrupt + msg = 'Timeout. Will retry in {0} second{1} ...'.format( + count, 's' if count != 1 else '') + self.callback([msg]) + time.sleep(1) + self.callback(['Retrying ...']) + + def try_command(self): + """ Execute a cmd & poll for callback. Returns list of output. + Raises CmdFailed -> return code for Popen isn't 0 + Raises CmdTimedOut -> command exceeded timeout without new output + """ + first_line = True + + try: + tfile = tempfile.NamedTemporaryFile(mode='w+b') + preexec_fn = not G_IS_WIN and os.setsid or None + self.proc = subprocess.Popen(self.cmd, stdout=tfile, + stderr=subprocess.STDOUT, + stdin=subprocess.PIPE, shell=True, + preexec_fn=preexec_fn) + thrd = thr.Thread(target=(lambda proc: proc.wait()), args=(self.proc,)) + thrd.start() + + thread_not_started = True + while thread_not_started: + try: + thrd.join(0.1) + thread_not_started = False + except RuntimeError: + pass + + while self.alive: + if G_STOP.is_set(): + raise KeyboardInterrupt + + if first_line or random.random() < G_LOG_PROB: + first_line = False + line = '' if G_IS_WIN else nonblock_read(tfile.name) + if line: + self.callback([line]) + + time_diff = time.time() - os.path.getmtime(tfile.name) + if time_diff > self.timeout: + raise CmdTimedOut(['Timeout!']) + + thrd.join(0.5) + + tfile.seek(0) + result = [line.decode('utf-8', 'replace').rstrip() for line in tfile] + + if self.proc.returncode != 0: + raise CmdFailed([''] + result) + + return result + except: + self.terminate() + raise + + def terminate(self): + """ Terminate process and cleanup. """ + if self.alive: + if G_IS_WIN: + os.kill(self.proc.pid, signal.SIGINT) + else: + os.killpg(self.proc.pid, signal.SIGTERM) + self.clean() + +class Plugin(object): + def __init__(self, name, args, buf_q, lock): + self.name = name + self.args = args + self.buf_q = buf_q + self.lock = lock + self.tag = args.get('tag', 0) + + def manage(self): + try: + if os.path.exists(self.args['dir']): + self.update() + else: + self.install() + with self.lock: + thread_vim_command("let s:update.new['{0}'] = 1".format(self.name)) + except PlugError as exc: + self.write(Action.ERROR, self.name, exc.msg) + except KeyboardInterrupt: + G_STOP.set() + self.write(Action.ERROR, self.name, ['Interrupted!']) + except: + # Any exception except those above print stack trace + msg = 'Trace:\n{0}'.format(traceback.format_exc().rstrip()) + self.write(Action.ERROR, self.name, msg.split('\n')) + raise + + def install(self): + target = self.args['dir'] + if target[-1] == '\\': + target = target[0:-1] + + def clean(target): + def _clean(): + try: + shutil.rmtree(target) + except OSError: + pass + return _clean + + self.write(Action.INSTALL, self.name, ['Installing ...']) + callback = functools.partial(self.write, Action.INSTALL, self.name) + cmd = 'git clone {0} {1} {2} {3} 2>&1'.format( + '' if self.tag else G_CLONE_OPT, G_PROGRESS, self.args['uri'], + esc(target)) + com = Command(cmd, None, G_TIMEOUT, callback, clean(target)) + result = com.execute(G_RETRIES) + self.write(Action.DONE, self.name, result[-1:]) + + def repo_uri(self): + cmd = 'git rev-parse --abbrev-ref HEAD 2>&1 && git config -f .git/config remote.origin.url' + command = Command(cmd, self.args['dir'], G_TIMEOUT,) + result = command.execute(G_RETRIES) + return result[-1] + + def update(self): + actual_uri = self.repo_uri() + expect_uri = self.args['uri'] + regex = re.compile(r'^(?:\w+://)?(?:[^@/]*@)?([^:/]*(?::[0-9]*)?)[:/](.*?)(?:\.git)?/?$') + ma = regex.match(actual_uri) + mb = regex.match(expect_uri) + if ma is None or mb is None or ma.groups() != mb.groups(): + msg = ['', + 'Invalid URI: {0}'.format(actual_uri), + 'Expected {0}'.format(expect_uri), + 'PlugClean required.'] + raise InvalidURI(msg) + + if G_PULL: + self.write(Action.UPDATE, self.name, ['Updating ...']) + callback = functools.partial(self.write, Action.UPDATE, self.name) + fetch_opt = '--depth 99999999' if self.tag and os.path.isfile(os.path.join(self.args['dir'], '.git/shallow')) else '' + cmd = 'git fetch {0} {1} 2>&1'.format(fetch_opt, G_PROGRESS) + com = Command(cmd, self.args['dir'], G_TIMEOUT, callback) + result = com.execute(G_RETRIES) + self.write(Action.DONE, self.name, result[-1:]) + else: + self.write(Action.DONE, self.name, ['Already installed']) + + def write(self, action, name, msg): + self.buf_q.put((action, name, msg)) + +class PlugThread(thr.Thread): + def __init__(self, tname, args): + super(PlugThread, self).__init__() + self.tname = tname + self.args = args + + def run(self): + thr.current_thread().name = self.tname + buf_q, work_q, lock = self.args + + try: + while not G_STOP.is_set(): + name, args = work_q.get_nowait() + plug = Plugin(name, args, buf_q, lock) + plug.manage() + work_q.task_done() + except queue.Empty: + pass + +class RefreshThread(thr.Thread): + def __init__(self, lock): + super(RefreshThread, self).__init__() + self.lock = lock + self.running = True + + def run(self): + while self.running: + with self.lock: + thread_vim_command('noautocmd normal! a') + time.sleep(0.33) + + def stop(self): + self.running = False + +if G_NVIM: + def thread_vim_command(cmd): + vim.session.threadsafe_call(lambda: vim.command(cmd)) +else: + def thread_vim_command(cmd): + vim.command(cmd) + +def esc(name): + return '"' + name.replace('"', '\"') + '"' + +def nonblock_read(fname): + """ Read a file with nonblock flag. Return the last line. """ + fread = os.open(fname, os.O_RDONLY | os.O_NONBLOCK) + buf = os.read(fread, 100000).decode('utf-8', 'replace') + os.close(fread) + + line = buf.rstrip('\r\n') + left = max(line.rfind('\r'), line.rfind('\n')) + if left != -1: + left += 1 + line = line[left:] + + return line + +def main(): + thr.current_thread().name = 'main' + nthreads = int(vim.eval('s:update.threads')) + plugs = vim.eval('s:update.todo') + mac_gui = vim.eval('s:mac_gui') == '1' + + lock = thr.Lock() + buf = Buffer(lock, len(plugs), G_PULL) + buf_q, work_q = queue.Queue(), queue.Queue() + for work in plugs.items(): + work_q.put(work) + + start_cnt = thr.active_count() + for num in range(nthreads): + tname = 'PlugT-{0:02}'.format(num) + thread = PlugThread(tname, (buf_q, work_q, lock)) + thread.start() + if mac_gui: + rthread = RefreshThread(lock) + rthread.start() + + while not buf_q.empty() or thr.active_count() != start_cnt: + try: + action, name, msg = buf_q.get(True, 0.25) + buf.write(action, name, ['OK'] if not msg else msg) + buf_q.task_done() + except queue.Empty: + pass + except KeyboardInterrupt: + G_STOP.set() + + if mac_gui: + rthread.stop() + rthread.join() + +main() +EOF +endfunction + +function! s:update_ruby() + ruby << EOF + module PlugStream + SEP = ["\r", "\n", nil] + def get_line + buffer = '' + loop do + char = readchar rescue return + if SEP.include? char.chr + buffer << $/ + break + else + buffer << char + end + end + buffer + end + end unless defined?(PlugStream) + + def esc arg + %["#{arg.gsub('"', '\"')}"] + end + + def killall pid + pids = [pid] + if /mswin|mingw|bccwin/ =~ RUBY_PLATFORM + pids.each { |pid| Process.kill 'INT', pid.to_i rescue nil } + else + unless `which pgrep 2> /dev/null`.empty? + children = pids + until children.empty? + children = children.map { |pid| + `pgrep -P #{pid}`.lines.map { |l| l.chomp } + }.flatten + pids += children + end + end + pids.each { |pid| Process.kill 'TERM', pid.to_i rescue nil } + end + end + + def compare_git_uri a, b + regex = %r{^(?:\w+://)?(?:[^@/]*@)?([^:/]*(?::[0-9]*)?)[:/](.*?)(?:\.git)?/?$} + regex.match(a).to_a.drop(1) == regex.match(b).to_a.drop(1) + end + + require 'thread' + require 'fileutils' + require 'timeout' + running = true + iswin = VIM::evaluate('s:is_win').to_i == 1 + pull = VIM::evaluate('s:update.pull').to_i == 1 + base = VIM::evaluate('g:plug_home') + all = VIM::evaluate('s:update.todo') + limit = VIM::evaluate('get(g:, "plug_timeout", 60)') + tries = VIM::evaluate('get(g:, "plug_retries", 2)') + 1 + nthr = VIM::evaluate('s:update.threads').to_i + maxy = VIM::evaluate('winheight(".")').to_i + vim7 = VIM::evaluate('v:version').to_i <= 703 && RUBY_PLATFORM =~ /darwin/ + cd = iswin ? 'cd /d' : 'cd' + tot = VIM::evaluate('len(s:update.todo)') || 0 + bar = '' + skip = 'Already installed' + mtx = Mutex.new + take1 = proc { mtx.synchronize { running && all.shift } } + logh = proc { + cnt = bar.length + $curbuf[1] = "#{pull ? 'Updating' : 'Installing'} plugins (#{cnt}/#{tot})" + $curbuf[2] = '[' + bar.ljust(tot) + ']' + VIM::command('normal! 2G') + VIM::command('redraw') + } + where = proc { |name| (1..($curbuf.length)).find { |l| $curbuf[l] =~ /^[-+x*] #{name}:/ } } + log = proc { |name, result, type| + mtx.synchronize do + ing = ![true, false].include?(type) + bar += type ? '=' : 'x' unless ing + b = case type + when :install then '+' when :update then '*' + when true, nil then '-' else + VIM::command("call add(s:update.errors, '#{name}')") + 'x' + end + result = + if type || type.nil? + ["#{b} #{name}: #{result.lines.to_a.last || 'OK'}"] + elsif result =~ /^Interrupted|^Timeout/ + ["#{b} #{name}: #{result}"] + else + ["#{b} #{name}"] + result.lines.map { |l| " " << l } + end + if lnum = where.call(name) + $curbuf.delete lnum + lnum = 4 if ing && lnum > maxy + end + result.each_with_index do |line, offset| + $curbuf.append((lnum || 4) - 1 + offset, line.gsub(/\e\[./, '').chomp) + end + logh.call + end + } + bt = proc { |cmd, name, type, cleanup| + tried = timeout = 0 + begin + tried += 1 + timeout += limit + fd = nil + data = '' + if iswin + Timeout::timeout(timeout) do + tmp = VIM::evaluate('tempname()') + system("(#{cmd}) > #{tmp}") + data = File.read(tmp).chomp + File.unlink tmp rescue nil + end + else + fd = IO.popen(cmd).extend(PlugStream) + first_line = true + log_prob = 1.0 / nthr + while line = Timeout::timeout(timeout) { fd.get_line } + data << line + log.call name, line.chomp, type if name && (first_line || rand < log_prob) + first_line = false + end + fd.close + end + [$? == 0, data.chomp] + rescue Timeout::Error, Interrupt => e + if fd && !fd.closed? + killall fd.pid + fd.close + end + cleanup.call if cleanup + if e.is_a?(Timeout::Error) && tried < tries + 3.downto(1) do |countdown| + s = countdown > 1 ? 's' : '' + log.call name, "Timeout. Will retry in #{countdown} second#{s} ...", type + sleep 1 + end + log.call name, 'Retrying ...', type + retry + end + [false, e.is_a?(Interrupt) ? "Interrupted!" : "Timeout!"] + end + } + main = Thread.current + threads = [] + watcher = Thread.new { + if vim7 + while VIM::evaluate('getchar(1)') + sleep 0.1 + end + else + require 'io/console' # >= Ruby 1.9 + nil until IO.console.getch == 3.chr + end + mtx.synchronize do + running = false + threads.each { |t| t.raise Interrupt } unless vim7 + end + threads.each { |t| t.join rescue nil } + main.kill + } + refresh = Thread.new { + while true + mtx.synchronize do + break unless running + VIM::command('noautocmd normal! a') + end + sleep 0.2 + end + } if VIM::evaluate('s:mac_gui') == 1 + + clone_opt = VIM::evaluate('s:clone_opt').join(' ') + progress = VIM::evaluate('s:progress_opt(1)') + nthr.times do + mtx.synchronize do + threads << Thread.new { + while pair = take1.call + name = pair.first + dir, uri, tag = pair.last.values_at *%w[dir uri tag] + exists = File.directory? dir + ok, result = + if exists + chdir = "#{cd} #{iswin ? dir : esc(dir)}" + ret, data = bt.call "#{chdir} && git rev-parse --abbrev-ref HEAD 2>&1 && git config -f .git/config remote.origin.url", nil, nil, nil + current_uri = data.lines.to_a.last + if !ret + if data =~ /^Interrupted|^Timeout/ + [false, data] + else + [false, [data.chomp, "PlugClean required."].join($/)] + end + elsif !compare_git_uri(current_uri, uri) + [false, ["Invalid URI: #{current_uri}", + "Expected: #{uri}", + "PlugClean required."].join($/)] + else + if pull + log.call name, 'Updating ...', :update + fetch_opt = (tag && File.exist?(File.join(dir, '.git/shallow'))) ? '--depth 99999999' : '' + bt.call "#{chdir} && git fetch #{fetch_opt} #{progress} 2>&1", name, :update, nil + else + [true, skip] + end + end + else + d = esc dir.sub(%r{[\\/]+$}, '') + log.call name, 'Installing ...', :install + bt.call "git clone #{clone_opt unless tag} #{progress} #{uri} #{d} 2>&1", name, :install, proc { + FileUtils.rm_rf dir + } + end + mtx.synchronize { VIM::command("let s:update.new['#{name}'] = 1") } if !exists && ok + log.call name, result, ok + end + } if running + end + end + threads.each { |t| t.join rescue nil } + logh.call + refresh.kill if refresh + watcher.kill +EOF +endfunction + +function! s:shellesc_cmd(arg, script) + let escaped = substitute('"'.a:arg.'"', '[&|<>()@^!"]', '^&', 'g') + return substitute(escaped, '%', (a:script ? '%' : '^') . '&', 'g') +endfunction + +function! s:shellesc_ps1(arg) + return "'".substitute(escape(a:arg, '\"'), "'", "''", 'g')."'" +endfunction + +function! s:shellesc_sh(arg) + return "'".substitute(a:arg, "'", "'\\\\''", 'g')."'" +endfunction + +" Escape the shell argument based on the shell. +" Vim and Neovim's shellescape() are insufficient. +" 1. shellslash determines whether to use single/double quotes. +" Double-quote escaping is fragile for cmd.exe. +" 2. It does not work for powershell. +" 3. It does not work for *sh shells if the command is executed +" via cmd.exe (ie. cmd.exe /c sh -c command command_args) +" 4. It does not support batchfile syntax. +" +" Accepts an optional dictionary with the following keys: +" - shell: same as Vim/Neovim 'shell' option. +" If unset, fallback to 'cmd.exe' on Windows or 'sh'. +" - script: If truthy and shell is cmd.exe, escape for batchfile syntax. +function! plug#shellescape(arg, ...) + if a:arg =~# '^[A-Za-z0-9_/:.-]\+$' + return a:arg + endif + let opts = a:0 > 0 && type(a:1) == s:TYPE.dict ? a:1 : {} + let shell = get(opts, 'shell', s:is_win ? 'cmd.exe' : 'sh') + let script = get(opts, 'script', 1) + if shell =~# 'cmd\(\.exe\)\?$' + return s:shellesc_cmd(a:arg, script) + elseif s:is_powershell(shell) + return s:shellesc_ps1(a:arg) + endif + return s:shellesc_sh(a:arg) +endfunction + +function! s:glob_dir(path) + return map(filter(s:glob(a:path, '**'), 'isdirectory(v:val)'), 's:dirpath(v:val)') +endfunction + +function! s:progress_bar(line, bar, total) + call setline(a:line, '[' . s:lpad(a:bar, a:total) . ']') +endfunction + +function! s:compare_git_uri(a, b) + " See `git help clone' + " https:// [user@] github.com[:port] / junegunn/vim-plug [.git] + " [git@] github.com[:port] : junegunn/vim-plug [.git] + " file:// / junegunn/vim-plug [/] + " / junegunn/vim-plug [/] + let pat = '^\%(\w\+://\)\='.'\%([^@/]*@\)\='.'\([^:/]*\%(:[0-9]*\)\=\)'.'[:/]'.'\(.\{-}\)'.'\%(\.git\)\=/\?$' + let ma = matchlist(a:a, pat) + let mb = matchlist(a:b, pat) + return ma[1:2] ==# mb[1:2] +endfunction + +function! s:format_message(bullet, name, message) + if a:bullet != 'x' + return [printf('%s %s: %s', a:bullet, a:name, s:lastline(a:message))] + else + let lines = map(s:lines(a:message), '" ".v:val') + return extend([printf('x %s:', a:name)], lines) + endif +endfunction + +function! s:with_cd(cmd, dir, ...) + let script = a:0 > 0 ? a:1 : 1 + let pwsh = s:is_powershell(&shell) + let cd = s:is_win && !pwsh ? 'cd /d' : 'cd' + let sep = pwsh ? ';' : '&&' + return printf('%s %s %s %s', cd, plug#shellescape(a:dir, {'script': script, 'shell': &shell}), sep, a:cmd) +endfunction + +function! s:system_job(cmd) abort + let tmp = tempname() + let job = job_start(['/bin/sh', '-c', a:cmd], { + \ 'out_io': 'file', + \ 'out_name': tmp, + \ 'err_io': 'out', + \}) + while job_status(job) ==# 'run' + sleep 1m + endwhile + let s:shell_error = job_info(job).exitval + let result = filereadable(tmp) ? join(readfile(tmp, 'b'), "\n") : '' + silent! call delete(tmp) + return result +endfunction + +function! s:system(cmd, ...) + let batchfile = '' + try + let [sh, shellcmdflag, shrd] = s:chsh(1) + if type(a:cmd) == s:TYPE.list + " Neovim's system() supports list argument to bypass the shell + " but it cannot set the working directory for the command. + " Assume that the command does not rely on the shell. + if has('nvim') && a:0 == 0 + let ret = system(a:cmd) + let s:shell_error = v:shell_error + return ret + endif + let cmd = join(map(copy(a:cmd), 'plug#shellescape(v:val, {"shell": &shell, "script": 0})')) + if s:is_powershell(&shell) + let cmd = '& ' . cmd + endif + else + let cmd = a:cmd + endif + if a:0 > 0 + let cmd = s:with_cd(cmd, a:1, type(a:cmd) != s:TYPE.list) + endif + if s:is_win && type(a:cmd) != s:TYPE.list + let [batchfile, cmd] = s:batchfile(cmd) + endif + if s:vim8 && has('gui_running') && !s:is_win + return s:system_job(cmd) + endif + let ret = system(cmd) + let s:shell_error = v:shell_error + return ret + finally + let [&shell, &shellcmdflag, &shellredir] = [sh, shellcmdflag, shrd] + if s:is_win && filereadable(batchfile) + call delete(batchfile) + endif + endtry +endfunction + +function! s:system_chomp(...) + let ret = call('s:system', a:000) + return s:shell_error ? '' : substitute(ret, '\n$', '', '') +endfunction + +function! s:git_validate(spec, check_branch) + let err = '' + if isdirectory(a:spec.dir) + let result = [s:git_local_branch(a:spec.dir), s:git_origin_url(a:spec.dir)] + let remote = result[-1] + if empty(remote) + let err = join([remote, 'PlugClean required.'], "\n") + elseif !s:compare_git_uri(remote, a:spec.uri) + let err = join(['Invalid URI: '.remote, + \ 'Expected: '.a:spec.uri, + \ 'PlugClean required.'], "\n") + elseif !a:check_branch + return ['', 0] + elseif has_key(a:spec, 'commit') + let sha = s:git_revision(a:spec.dir) + if empty(sha) + let err = join(add(result, 'PlugClean required.'), "\n") + elseif !s:hash_match(sha, a:spec.commit) + let err = join([printf('Invalid HEAD (expected: %s, actual: %s)', + \ a:spec.commit[:6], sha[:6]), + \ 'PlugUpdate required.'], "\n") + endif + elseif has_key(a:spec, 'tag') + let tag = s:system_chomp('git describe --exact-match --tags HEAD 2>&1', a:spec.dir) + if a:spec.tag !=# tag && a:spec.tag !~ '\*' + let err = printf('Invalid tag: %s (expected: %s). Try PlugUpdate.', + \ (empty(tag) ? 'N/A' : tag), a:spec.tag) + endif + elseif a:check_branch + let current_branch = result[0] + let origin_branch = s:git_origin_branch(a:spec) + if origin_branch !=# current_branch + let err = printf('Invalid branch: %s (expected: %s). Try PlugUpdate.', + \ current_branch, origin_branch) + endif + if empty(err) + let ahead_behind = split(s:lastline(s:system([ + \ 'git', 'rev-list', '--count', '--left-right', + \ printf('HEAD...origin/%s', origin_branch) + \ ], a:spec.dir)), '\t') + if s:shell_error || len(ahead_behind) != 2 + let err = "Failed to compare with the origin. The default branch might have changed.\nPlugClean required." + else + let [ahead, behind] = ahead_behind + if ahead && behind + " Only mention PlugClean if diverged, otherwise it's likely to be + " pushable (and probably not that messed up). + let err = printf( + \ "Diverged from origin/%s (%d commit(s) ahead and %d commit(s) behind!\n" + \ .'Backup local changes and run PlugClean and PlugUpdate to reinstall it.', origin_branch, ahead, behind) + elseif ahead + let err = printf("Ahead of origin/%s by %d commit(s).\n" + \ .'Cannot update until local changes are pushed.', + \ origin_branch, ahead) + endif + endif + endif + endif + else + let err = 'Not found' + endif + return [err, err =~# 'PlugClean'] +endfunction + +function! s:rm_rf(dir) + if isdirectory(a:dir) + return s:system(s:is_win + \ ? 'rmdir /S /Q '.plug#shellescape(a:dir) + \ : ['rm', '-rf', a:dir]) + endif +endfunction + +function! s:clean(force) + call s:prepare() + call append(0, 'Searching for invalid plugins in '.g:plug_home) + call append(1, '') + + " List of valid directories + let dirs = [] + let errs = {} + let [cnt, total] = [0, len(g:plugs)] + for [name, spec] in items(g:plugs) + if !s:is_managed(name) || get(spec, 'frozen', 0) + call add(dirs, spec.dir) + else + let [err, clean] = s:git_validate(spec, 1) + if clean + let errs[spec.dir] = s:lines(err)[0] + else + call add(dirs, spec.dir) + endif + endif + let cnt += 1 + call s:progress_bar(2, repeat('=', cnt), total) + normal! 2G + redraw + endfor + + let allowed = {} + for dir in dirs + let allowed[s:dirpath(s:plug_fnamemodify(dir, ':h:h'))] = 1 + let allowed[dir] = 1 + for child in s:glob_dir(dir) + let allowed[child] = 1 + endfor + endfor + + let todo = [] + let found = sort(s:glob_dir(g:plug_home)) + while !empty(found) + let f = remove(found, 0) + if !has_key(allowed, f) && isdirectory(f) + call add(todo, f) + call append(line('$'), '- ' . f) + if has_key(errs, f) + call append(line('$'), ' ' . errs[f]) + endif + let found = filter(found, 'stridx(v:val, f) != 0') + end + endwhile + + 4 + redraw + if empty(todo) + call append(line('$'), 'Already clean.') + else + let s:clean_count = 0 + call append(3, ['Directories to delete:', '']) + redraw! + if a:force || s:ask_no_interrupt('Delete all directories?') + call s:delete([6, line('$')], 1) + else + call setline(4, 'Cancelled.') + nnoremap d :set opfunc=delete_opg@ + nmap dd d_ + xnoremap d :call delete_op(visualmode(), 1) + echo 'Delete the lines (d{motion}) to delete the corresponding directories' + endif + endif + 4 + setlocal nomodifiable +endfunction + +function! s:delete_op(type, ...) + call s:delete(a:0 ? [line("'<"), line("'>")] : [line("'["), line("']")], 0) +endfunction + +function! s:delete(range, force) + let [l1, l2] = a:range + let force = a:force + let err_count = 0 + while l1 <= l2 + let line = getline(l1) + if line =~ '^- ' && isdirectory(line[2:]) + execute l1 + redraw! + let answer = force ? 1 : s:ask('Delete '.line[2:].'?', 1) + let force = force || answer > 1 + if answer + let err = s:rm_rf(line[2:]) + setlocal modifiable + if empty(err) + call setline(l1, '~'.line[1:]) + let s:clean_count += 1 + else + delete _ + call append(l1 - 1, s:format_message('x', line[1:], err)) + let l2 += len(s:lines(err)) + let err_count += 1 + endif + let msg = printf('Removed %d directories.', s:clean_count) + if err_count > 0 + let msg .= printf(' Failed to remove %d directories.', err_count) + endif + call setline(4, msg) + setlocal nomodifiable + endif + endif + let l1 += 1 + endwhile +endfunction + +function! s:upgrade() + echo 'Downloading the latest version of vim-plug' + redraw + let tmp = s:plug_tempname() + let new = tmp . '/plug.vim' + + try + let out = s:system(['git', 'clone', '--depth', '1', s:plug_src, tmp]) + if s:shell_error + return s:err('Error upgrading vim-plug: '. out) + endif + + if readfile(s:me) ==# readfile(new) + echo 'vim-plug is already up-to-date' + return 0 + else + call rename(s:me, s:me . '.old') + call rename(new, s:me) + unlet g:loaded_plug + echo 'vim-plug has been upgraded' + return 1 + endif + finally + silent! call s:rm_rf(tmp) + endtry +endfunction + +function! s:upgrade_specs() + for spec in values(g:plugs) + let spec.frozen = get(spec, 'frozen', 0) + endfor +endfunction + +function! s:status() + call s:prepare() + call append(0, 'Checking plugins') + call append(1, '') + + let ecnt = 0 + let unloaded = 0 + let [cnt, total] = [0, len(g:plugs)] + for [name, spec] in items(g:plugs) + let is_dir = isdirectory(spec.dir) + if has_key(spec, 'uri') + if is_dir + let [err, _] = s:git_validate(spec, 1) + let [valid, msg] = [empty(err), empty(err) ? 'OK' : err] + else + let [valid, msg] = [0, 'Not found. Try PlugInstall.'] + endif + else + if is_dir + let [valid, msg] = [1, 'OK'] + else + let [valid, msg] = [0, 'Not found.'] + endif + endif + let cnt += 1 + let ecnt += !valid + " `s:loaded` entry can be missing if PlugUpgraded + if is_dir && get(s:loaded, name, -1) == 0 + let unloaded = 1 + let msg .= ' (not loaded)' + endif + call s:progress_bar(2, repeat('=', cnt), total) + call append(3, s:format_message(valid ? '-' : 'x', name, msg)) + normal! 2G + redraw + endfor + call setline(1, 'Finished. '.ecnt.' error(s).') + normal! gg + setlocal nomodifiable + if unloaded + echo "Press 'L' on each line to load plugin, or 'U' to update" + nnoremap L :call status_load(line('.')) + xnoremap L :call status_load(line('.')) + end +endfunction + +function! s:extract_name(str, prefix, suffix) + return matchstr(a:str, '^'.a:prefix.' \zs[^:]\+\ze:.*'.a:suffix.'$') +endfunction + +function! s:status_load(lnum) + let line = getline(a:lnum) + let name = s:extract_name(line, '-', '(not loaded)') + if !empty(name) + call plug#load(name) + setlocal modifiable + call setline(a:lnum, substitute(line, ' (not loaded)$', '', '')) + setlocal nomodifiable + endif +endfunction + +function! s:status_update() range + let lines = getline(a:firstline, a:lastline) + let names = filter(map(lines, 's:extract_name(v:val, "[x-]", "")'), '!empty(v:val)') + if !empty(names) + echo + execute 'PlugUpdate' join(names) + endif +endfunction + +function! s:is_preview_window_open() + silent! wincmd P + if &previewwindow + wincmd p + return 1 + endif +endfunction + +function! s:find_name(lnum) + for lnum in reverse(range(1, a:lnum)) + let line = getline(lnum) + if empty(line) + return '' + endif + let name = s:extract_name(line, '-', '') + if !empty(name) + return name + endif + endfor + return '' +endfunction + +function! s:preview_commit() + if b:plug_preview < 0 + let b:plug_preview = !s:is_preview_window_open() + endif + + let sha = matchstr(getline('.'), '^ \X*\zs[0-9a-f]\{7,9}') + if empty(sha) + let name = matchstr(getline('.'), '^- \zs[^:]*\ze:$') + if empty(name) + return + endif + let title = 'HEAD@{1}..' + let command = 'git diff --no-color HEAD@{1}' + else + let title = sha + let command = 'git show --no-color --pretty=medium '.sha + let name = s:find_name(line('.')) + endif + + if empty(name) || !has_key(g:plugs, name) || !isdirectory(g:plugs[name].dir) + return + endif + + if !s:is_preview_window_open() + execute get(g:, 'plug_pwindow', 'vertical rightbelow new') + execute 'e' title + else + execute 'pedit' title + wincmd P + endif + setlocal previewwindow filetype=git buftype=nofile bufhidden=wipe nobuflisted modifiable + let batchfile = '' + try + let [sh, shellcmdflag, shrd] = s:chsh(1) + let cmd = 'cd '.plug#shellescape(g:plugs[name].dir).' && '.command + if s:is_win + let [batchfile, cmd] = s:batchfile(cmd) + endif + execute 'silent %!' cmd + finally + let [&shell, &shellcmdflag, &shellredir] = [sh, shellcmdflag, shrd] + if s:is_win && filereadable(batchfile) + call delete(batchfile) + endif + endtry + setlocal nomodifiable + nnoremap q :q + wincmd p +endfunction + +function! s:section(flags) + call search('\(^[x-] \)\@<=[^:]\+:', a:flags) +endfunction + +function! s:format_git_log(line) + let indent = ' ' + let tokens = split(a:line, nr2char(1)) + if len(tokens) != 5 + return indent.substitute(a:line, '\s*$', '', '') + endif + let [graph, sha, refs, subject, date] = tokens + let tag = matchstr(refs, 'tag: [^,)]\+') + let tag = empty(tag) ? ' ' : ' ('.tag.') ' + return printf('%s%s%s%s%s (%s)', indent, graph, sha, tag, subject, date) +endfunction + +function! s:append_ul(lnum, text) + call append(a:lnum, ['', a:text, repeat('-', len(a:text))]) +endfunction + +function! s:diff() + call s:prepare() + call append(0, ['Collecting changes ...', '']) + let cnts = [0, 0] + let bar = '' + let total = filter(copy(g:plugs), 's:is_managed(v:key) && isdirectory(v:val.dir)') + call s:progress_bar(2, bar, len(total)) + for origin in [1, 0] + let plugs = reverse(sort(items(filter(copy(total), (origin ? '' : '!').'(has_key(v:val, "commit") || has_key(v:val, "tag"))')))) + if empty(plugs) + continue + endif + call s:append_ul(2, origin ? 'Pending updates:' : 'Last update:') + for [k, v] in plugs + let branch = s:git_origin_branch(v) + if len(branch) + let range = origin ? '..origin/'.branch : 'HEAD@{1}..' + let cmd = ['git', 'log', '--graph', '--color=never'] + if s:git_version_requirement(2, 10, 0) + call add(cmd, '--no-show-signature') + endif + call extend(cmd, ['--pretty=format:%x01%h%x01%d%x01%s%x01%cr', range]) + if has_key(v, 'rtp') + call extend(cmd, ['--', v.rtp]) + endif + let diff = s:system_chomp(cmd, v.dir) + if !empty(diff) + let ref = has_key(v, 'tag') ? (' (tag: '.v.tag.')') : has_key(v, 'commit') ? (' '.v.commit) : '' + call append(5, extend(['', '- '.k.':'.ref], map(s:lines(diff), 's:format_git_log(v:val)'))) + let cnts[origin] += 1 + endif + endif + let bar .= '=' + call s:progress_bar(2, bar, len(total)) + normal! 2G + redraw + endfor + if !cnts[origin] + call append(5, ['', 'N/A']) + endif + endfor + call setline(1, printf('%d plugin(s) updated.', cnts[0]) + \ . (cnts[1] ? printf(' %d plugin(s) have pending updates.', cnts[1]) : '')) + + if cnts[0] || cnts[1] + nnoremap (plug-preview) :silent! call preview_commit() + if empty(maparg("\", 'n')) + nmap (plug-preview) + endif + if empty(maparg('o', 'n')) + nmap o (plug-preview) + endif + endif + if cnts[0] + nnoremap X :call revert() + echo "Press 'X' on each block to revert the update" + endif + normal! gg + setlocal nomodifiable +endfunction + +function! s:revert() + if search('^Pending updates', 'bnW') + return + endif + + let name = s:find_name(line('.')) + if empty(name) || !has_key(g:plugs, name) || + \ input(printf('Revert the update of %s? (y/N) ', name)) !~? '^y' + return + endif + + call s:system('git reset --hard HEAD@{1} && git checkout '.plug#shellescape(g:plugs[name].branch).' --', g:plugs[name].dir) + setlocal modifiable + normal! "_dap + setlocal nomodifiable + echo 'Reverted' +endfunction + +function! s:snapshot(force, ...) abort + call s:prepare() + setf vim + call append(0, ['" Generated by vim-plug', + \ '" '.strftime("%c"), + \ '" :source this file in vim to restore the snapshot', + \ '" or execute: vim -S snapshot.vim', + \ '', '', 'PlugUpdate!']) + 1 + let anchor = line('$') - 3 + let names = sort(keys(filter(copy(g:plugs), + \'has_key(v:val, "uri") && isdirectory(v:val.dir)'))) + for name in reverse(names) + let sha = has_key(g:plugs[name], 'commit') ? g:plugs[name].commit : s:git_revision(g:plugs[name].dir) + if !empty(sha) + call append(anchor, printf("silent! let g:plugs['%s'].commit = '%s'", name, sha)) + redraw + endif + endfor + + if a:0 > 0 + let fn = s:plug_expand(a:1) + if filereadable(fn) && !(a:force || s:ask(a:1.' already exists. Overwrite?')) + return + endif + call writefile(getline(1, '$'), fn) + echo 'Saved as '.a:1 + silent execute 'e' s:esc(fn) + setf vim + endif +endfunction + +function! s:split_rtp() + return split(&rtp, '\\\@ :so $MYVIMRC + +" Enable local .vimrc config +set exrc +set secure + +" Lines of memory to remember +set history=10000 + +" Leader key to add extra key combinations +let mapleader = ',' +let g:mapleader = ',' + +" Time delay on key +set timeoutlen=3000 ttimeoutlen=100 + +" Update time +set updatetime=250 + +" Trigger InsertLeave autocmd +inoremap + +" No need for Ex mode +nnoremap Q + +" Open help in a vertical window +cnoreabbrev help vert help + +" Terminal +if has("terminal") + nnoremap :call ToggleTerminal() + tnoremap :wincmd p + tnoremap +endif + +" Set inc/dec +set nrformats-=octal + +"---------------------------------------------------------------- +" 2. Plugins (Plug) +"---------------------------------------------------------------- +" List of plugins installed +call plug#begin('~/.vim/plugged') + + " Statusbar + Plug 'vim-airline/vim-airline' + " Plug 'vim-airline/vim-airline-themes' + Plug 'gerardbm/vim-airline-themes' + + " Git tools + Plug 'airblade/vim-gitgutter' + Plug 'tpope/vim-fugitive' + + " Sessions + Plug 'xolox/vim-session' + Plug 'xolox/vim-misc' + + " Tools + Plug 'preservim/nerdcommenter' + Plug 'preservim/nerdtree' + Plug 'valloric/listtoggle' + Plug 'majutsushi/tagbar' + Plug 'dense-analysis/ale' + Plug 'junegunn/fzf' + Plug 'junegunn/fzf.vim' + Plug 'mbbill/undotree' + + " LSP and autocompletion + Plug 'neoclide/coc.nvim', {'branch': 'release'} + Plug 'honza/vim-snippets' + + " Go support + Plug 'fatih/vim-go', + + " Ruby support + Plug 'vim-ruby/vim-ruby' + Plug 'tpope/vim-rails' + Plug 'tpope/vim-endwise' + Plug 'tpope/vim-liquid' + + " JavaScript support + Plug 'othree/jspc.vim' + Plug 'maksimr/vim-jsbeautify' + + " Additional syntax files + Plug 'othree/html5.vim' + Plug 'vim-language-dept/css-syntax.vim' + Plug 'pangloss/vim-javascript' + Plug 'mboughaba/i3config.vim' + Plug 'aklt/plantuml-syntax' + Plug 'gerardbm/asy.vim' + Plug 'gerardbm/eukleides.vim' + Plug 'zaid/vim-rec' + Plug 'sirtaj/vim-openscad' + + " Edition + Plug 'junegunn/vim-easy-align' + Plug 'godlygeek/tabular' + Plug 'jiangmiao/auto-pairs' + Plug 'alvan/vim-closetag' + Plug 'tpope/vim-surround' + Plug 'tpope/vim-repeat' + Plug 'tpope/vim-capslock' + Plug 'wellle/targets.vim' + Plug 'christoomey/vim-sort-motion' + Plug 'terryma/vim-expand-region' + Plug 'Valloric/MatchTagAlways' + Plug 'FooSoft/vim-argwrap' + Plug 'gerardbm/vim-md-headings' + Plug 'gerardbm/vim-md-checkbox' + Plug 'matze/vim-move' + + " Misc + Plug 'christoomey/vim-tmux-navigator' + Plug 'tpope/vim-characterize' + Plug 'tyru/open-browser.vim' + Plug 'junegunn/goyo.vim' + Plug 'mattn/webapi-vim' + Plug 'mattn/emmet-vim' + Plug 'vimwiki/vimwiki', { 'branch': 'master' } + Plug 'chrisbra/colorizer' + + " Color schemes + Plug 'gerardbm/vim-atomic' + Plug 'gerardbm/vim-cosmic' + +call plug#end() + +"---------------------------------------------------------------- +" 3. Plugins settings +"---------------------------------------------------------------- +" --- Statusbar --- +" Airline settings +let g:airline_theme = 'atomic' +let g:airline_powerline_fonts = 1 +let g:airline#extensions#tabline#enabled = 1 +let g:airline#extensions#tabline#fnamemod = ':t' +let g:airline_section_z = airline#section#create([ + \ '%1p%% ', + \ 'Ξ%l%', + \ '\⍿%c']) +call airline#parts#define_accent('mode', 'black') + +" --- Git tools --- +" Gitgutter settings +let g:gitgutter_max_signs = 5000 +let g:gitgutter_sign_added = '+' +let g:gitgutter_sign_modified = '»' +let g:gitgutter_sign_removed = '_' +let g:gitgutter_sign_modified_removed = '»╌' +let g:gitgutter_map_keys = 0 +let g:gitgutter_diff_args = '--ignore-space-at-eol' + +nmap j (GitGutterNextHunk)zz +nmap k (GitGutterPrevHunk)zz +nnoremap :call ToggleGGPrev()zz +nnoremap ga :GitGutterStageHunk +nnoremap gu :GitGutterUndoHunk + +" Fugitive settings +nnoremap :call ToggleGstatus() +nnoremap gv :Gvdiffsplit:windo set wrap +nnoremap gh :Gvdiffsplit HEAD:windo set wrap +nnoremap gb :Git blame + +" Searching for text added or removed by a commit +nnoremap gg :call GrepWrapper('Gclog', '-i -G', '--') + +" --- Sessions --- +" Vim-session settings +let g:session_autosave = 'no' +let g:session_autoload = 'no' +let g:session_directory = '~/.vim/sessions/' + +" --- Tools --- +" NERDCommenter settings +let g:NERDDefaultAlign = 'left' +let g:NERDSpaceDelims = 1 +let g:NERDCompactSexyComs = 1 +let g:NERDCommentEmptyLines = 0 +let g:NERDCreateDefaultMappings = 0 +let g:NERDCustomDelimiters = { + \ 'python': {'left': '#'}, + \ } + +nnoremap cc :call nerdcommenter#Comment(0, 'toggle') +vnoremap cc :call nerdcommenter#Comment(0, 'toggle') + +" NERDTree settings +nnoremap :call ToggleNERDTree() + +" Listtoggle settings +let g:lt_location_list_toggle_map = 'e' +let g:lt_quickfix_list_toggle_map = 'q' + +" Tagbar toggle (custom function) +nnoremap :call ToggleTagbar() +let g:tagbar_autofocus = 1 +let g:tagbar_show_linenumbers = 2 +let g:tagbar_sort = 0 + +" ALE settings +let g:ale_enabled = 0 +let g:ale_sign_error='✗' +let g:ale_sign_warning='•' +let g:ale_sign_info='ℹ' +let g:ale_virtualtext_cursor=0 + +" FZF settings +let $FZF_DEFAULT_COMMAND = "ag --hidden --ignore .git -g ''" +let $FZF_PREVIEW_COMMAND = 'cat {}' +let g:fzf_preview_window = ['right', 'ctrl-i'] +nnoremap :Files +nnoremap :Buffers +nnoremap gz :Commits +nnoremap uh :History +nnoremap ul :BLines +nnoremap um :BMarks +nnoremap uu :BTags +nnoremap uf + \ :call fzf#vim#buffer_tags('', + \ { 'options': ['--nth', '..-2,-1', '--query', '^f$ '] }) +nnoremap uv + \ :call fzf#vim#buffer_tags('', + \ { 'options': ['--nth', '..-2,-1', '--query', '^v$ '] }) +nnoremap ut :Tags + +" Undotree toggle +nnoremap U :UndotreeToggle + +" --- LSP and autocompletion --- +" Coc.nvim settings +set signcolumn=yes +let g:coc_diagnostic_enable = 1 +let g:coc_snippet_next = '' +let g:coc_snippet_prev = '' + +imap (coc-snippets-expand-jump) +smap (coc-snippets-expand-jump) +xmap (coc-snippets-select) + +inoremap + \ coc#pum#visible() ? coc#_select_confirm() : + \ coc#expandableOrJumpable() ? + \ "\=coc#rpc#request('doKeymap', ['snippets-expand-jump',''])\" : + \ CheckBackspace() ? "\" : + \ coc#refresh() + +nnoremap E :CocDiagnostics --all +nnoremap cod (coc-definition) +nnoremap cot (coc-type-definition) +nnoremap coc (coc-implementation) +nnoremap cor (coc-references) + +" Navigate between errors +nnoremap h :lpreviouszz +nnoremap l :lnextzz + +" --- Languages --- +" Go settings +let g:go_highlight_functions = 1 +let g:go_highlight_methods = 1 +let g:go_highlight_fields = 1 +let g:go_highlight_types = 1 +let g:go_highlight_operators = 1 +let g:go_highlight_build_constraints = 1 +let g:go_bin_path = expand('~/.gotools') +let g:go_list_type = 'quickfix' + +" CSS3 settings +augroup VimCSS3Syntax + autocmd! + autocmd FileType css setlocal iskeyword+=- +augroup END + +" Javascript settings +let g:javascript_plugin_jsdoc = 1 +let g:javascript_plugin_ngdoc = 1 +let g:javascript_plugin_flow = 0 + +" JS-Beautify +let g:config_Beautifier = {} +let g:config_Beautifier['js'] = {} +let g:config_Beautifier['js'].indent_style = 'tab' +let g:config_Beautifier['jsx'] = {} +let g:config_Beautifier['jsx'].indent_style = 'tab' +let g:config_Beautifier['json'] = {} +let g:config_Beautifier['json'].indent_style = 'tab' +let g:config_Beautifier['css'] = {} +let g:config_Beautifier['css'].indent_style = 'tab' +let g:config_Beautifier['html'] = {} +let g:config_Beautifier['html'].indent_style = 'tab' + +augroup beautify + autocmd! + autocmd FileType javascript nnoremap bf :call JsBeautify() + autocmd FileType javascript vnoremap bf :call RangeJsBeautify() + autocmd FileType json nnoremap bf :call JsonBeautify() + autocmd FileType json vnoremap bf :call RangeJsonBeautify() + autocmd FileType jsx nnoremap bf :call JsxBeautify() + autocmd FileType jsx vnoremap bf :call RangeJsxBeautify() + autocmd FileType html nnoremap bf :call HtmlBeautify() + autocmd FileType html vnoremap bf :call RangeHtmlBeautify() + autocmd FileType css nnoremap bf :call CSSBeautify() + autocmd FileType css vnoremap bf :call RangeCSSBeautify() +augroup end + +" For conceal markers +if has('conceal') + set conceallevel=0 concealcursor=niv + nnoremap coi :set conceallevel=0:set concealcursor=niv + nnoremap coo :set conceallevel=2:set concealcursor=vc + nnoremap cop :set conceallevel=2:set concealcursor=niv + nnoremap com :set conceallevel=3:set concealcursor=niv +endif + +" --- Edition --- +" Easy align settings +xmap gi (EasyAlign) +nmap gi (EasyAlign) + +" Tabularize (e.g. /= or /:) +vnoremap x :Tabularize / + +" Tabularize only the first match on the line (e.g. /=.*/) +vnoremap X :Tabularize /.*/ + +" Auto-pairs settings +" Maps for normal and insert modes +let g:AutoPairsFlyMode = 0 +let g:AutoPairsMultilineClose = 0 +let g:AutoPairsShortcutJump = '' +let g:AutoPairsShortcutToggle = '' + +" Workaround to fix an Auto-pairs bug until it gets fixed +if has("nvim") + autocmd VimEnter,BufEnter,BufWinEnter * silent! iunmap +endif + +" Closetag settings +let g:closetag_filenames = '*.html,*.xhtml,*.phtml,*.xml,*.html.erb,*.md' + +" Surround settings +" Use 'yss?', 'yss%' or 'yss=' to surround a line +autocmd FileType php let b:surround_{char2nr('p')} = "" +autocmd FileType erb let b:surround_{char2nr('=')} = "<%= \r %>" +autocmd FileType erb let b:surround_{char2nr('-')} = "<% \r %>" +autocmd FileType html,markdown,liquid let b:surround_{char2nr('p')} = "{% \r %}" +autocmd FileType markdown,liquid let b:surround_{char2nr('i')} = "_\r_" +autocmd FileType markdown,liquid let b:surround_{char2nr('o')} = "**\r**" +autocmd FileType markdown,liquid let b:surround_{char2nr('x')} = "«\r»" +autocmd FileType markdown,liquid let b:surround_{char2nr('h')} = "\[\r\]\(//\)" +autocmd FileType markdown,liquid let b:surround_{char2nr('j')} = "!\[\r\] + \\(/images/\){: .align-}" +autocmd FileType markdown,liquid let b:surround_{char2nr('e')} = "\[\r\] + \\(\){:rel=\"noopener noreferrer\" target=\"_blank\"}" +autocmd FileType markdown,liquid let b:surround_{char2nr('y')} = "\r<\/a>" + +" Caps Lock settings +imap CapsLockToggle +cmap CapsLockToggle + +" Expand region settings +vmap v (expand_region_expand) +vmap m (expand_region_shrink) + +" MatchTagAlways settings +let g:mta_filetypes = { + \ 'html' : 1, + \ 'xhtml' : 1, + \ 'xml' : 1, + \ 'jinja' : 1, + \ 'php' : 1, + \ } + +" ArgWrap settings +let g:argwrap_tail_comma = 1 +let g:argwrap_padded_braces = '[{' + +nnoremap W :ArgWrap + +" Vim-move settings. Use Shift +let g:move_key_modifier = 'S' +let g:move_key_modifier_visualmode = 'S' + +" --- Misc --- +" Vim-tmux navigator settings +let g:tmux_navigator_no_mappings = 1 + +" Open-browser settings +let g:openbrowser_browser_commands = [{ + \ 'name': 'w3m', + \ 'args': 'tmux new-window w3m {uri}', + \ }] + +nmap gl (openbrowser-open) + +" Goyo settings +let g:goyo_width = "80" +let g:goyo_height = "100%" +let g:goyo_linenr = 1 + +nnoremap :Goyo + +" Vimwiki settings +let g:vimwiki_url_maxsave = 0 +let g:vimwiki_global_ext = 0 +let g:vimwiki_list = [ + \ {'path': '~/Workspace/vimwiki'}, + \ {'path': '~/Workspace/vimwiki/Articles'}, + \ {'path': '~/Workspace/vimwiki/Codes'}, + \ {'path': '~/Workspace/vimwiki/Notes'}, + \ {'path': '~/Workspace/vimwiki/Projects'}, + \ {'path': '~/Workspace/vimwiki/Studies'}, + \ {'path': '~/Workspace/vimwiki/ToDos'}, + \ {'path': '~/Workspace/vimwiki/Unix'} + \ ] + +nnoremap we :VimwikiToggleListItem +vnoremap we :VimwikiToggleListItem + +"---------------------------------------------------------------- +" 4. User interface +"---------------------------------------------------------------- +" Set X lines to the cursor when moving vertically +set scrolloff=0 + +" Always show mode +set showmode + +" Show command keys pressed +set showcmd + +" Enable the WiLd menu +set wildmenu + +" Show the current position +set ruler + +" Command bar height +set cmdheight=2 + +" Backspace works on Insert mode +set backspace=eol,start,indent + +" Don't redraw while executing macros (good performance config) +set lazyredraw + +" Show matching brackets when text indicator is over them +set showmatch + +" How many tenths of a second to blink when matching brackets +set matchtime=2 + +" No annoying sound on errors +set noerrorbells +set novisualbell + +" Mouse +set mouse=a + +" Highlight cursor line and cursor column +set cursorline +set nocursorcolumn + +" Always show the status line +set laststatus=2 + +" Change the cursor shape +if !has("nvim") + if &term != "linux" + let &t_SI = "\[6 q" + let &t_SR = "\[4 q" + let &t_EI = "\[2 q" + endif +else + set guicursor=n-v:block-Cursor/lCursor-blinkon0 + set guicursor+=i-ci-c:ver100-Cursor/lCursor-blinkon0 + set guicursor+=r-cr:hor100-Cursor/lCursor-blinkon0 +endif + +" Omni completion +if has('autocmd') && exists('+omnifunc') +autocmd Filetype * + \ if &omnifunc == "" | + \ setlocal omnifunc=syntaxcomplete#Complete | + \ endif +endif + +" Fix italics issue +if !has("nvim") + let &t_ZH="\e[3m" + let &t_ZR="\e[23m" +endif + +"---------------------------------------------------------------- +" 5. Scheme and colors +"---------------------------------------------------------------- +" True color +" if !has("nvim") +" if has("termguicolors") +" let &t_8f = "\[38;2;%lu;%lu;%lum" +" let &t_8b = "\[48;2;%lu;%lu;%lum" +" set termguicolors +" endif +" else +" set termguicolors +" endif + +" Syntax highlighting +syntax enable + +" Color scheme +let g:cosmic_matchparen = 0 +colorscheme cosmic + +" Show syntax highlighting groups +nnoremap B :call SynStack() + +"---------------------------------------------------------------- +" 6. Files and backup +"---------------------------------------------------------------- +" Disable swap files +set noswapfile + +" No backup (use Git instead) +set nobackup + +" Prevents automatic write backup +set nowritebackup + +" Use UTF-8 as default encoding +set encoding=utf8 + +" Use Unix as the standard file type +set fileformats=unix,dos,mac + +" Autoread a file when it is changed from the outside +set autoread + +" Reload a file when it is changed from the outside +let g:f5msg = 'Buffer reloaded.' +nnoremap :e:echo g:f5msg + +" Enable filetype plugins +filetype plugin on +filetype indent on + +" Allow us to use Ctrl-s and Ctrl-q as keybinds +" Restore default behaviour when leaving Vim. +if !has("nvim") + silent !stty -ixon + autocmd VimLeave * silent !stty ixon +endif + +" Save the current buffer +nnoremap s :update + +" Save all buffers +nnoremap S :bufdo update + +" :W sudo saves the file +" (useful for handling the permission-denied error) +cnoremap WW w !sudo tee > /dev/null % + +" Rename file +nnoremap :call RenameFile() + +" Work on buffer +nnoremap yab :%y +nnoremap dab :%d +nnoremap vab ggVG + +"---------------------------------------------------------------- +" 7. Buffers management +"---------------------------------------------------------------- +" Buffer hidden when it is abandoned +set hidden + +" Close the current buffer +nnoremap bd :call CustomCloseBuffer() + +" Move between buffers +nnoremap :bprev +nnoremap :bnext + +" Edit and explore buffers +nnoremap bb :edit =expand("%:p:h")/ +nnoremap bg :buffers:buffer + +" Copy the filepath to clipboard +nnoremap by :let @+=expand("%:p") + +" Switch CWD to the directory of the current buffer +nnoremap dd :lcd %:p:h:pwd + +" Switch CWD to git root directory +nnoremap dg :call GitRoot() + +" Ignore case when autocompletes when browsing files +set fileignorecase + +" Specify the behavior when switching between buffers +try + set switchbuf=useopen,usetab,newtab + set showtabline=2 +catch +endtry + +" Remember info about open buffers on close +" set viminfo^=% + +"---------------------------------------------------------------- +" 8. Tabs management +"---------------------------------------------------------------- +" Create and close tabs +nnoremap td :tabclose +nnoremap to :tabonly + +" Open a new tab with the current buffer's path +" Useful when editing files in the same directory +nnoremap tt :tabedit =expand("%:p:h")/ + +" Move tabs position +nnoremap tr :execute 'silent! tabmove ' . (tabpagenr()-2) +nnoremap ty :execute 'silent! tabmove ' . tabpagenr() + +"---------------------------------------------------------------- +" 9. Multiple windows +"---------------------------------------------------------------- +" Remap wincmd +map , + +set winminheight=0 +set winminwidth=0 +set splitbelow +set splitright +set fillchars+=stlnc:―,vert:│,fold:―,diff:― + +" Split windows +map - :split +map . :vsplit +map j :close +map x :q! +map , = + +" Resize windows +if bufwinnr(1) + map + :resize +1 + map - :resize -1 + map < :vertical resize +1 + map > :vertical resize -1 +endif + +" Toggle resize window +nnoremap f :call ToggleResize() + +" Last, previous and next window; and only one window +nnoremap l :wincmd p:echo "Last window." +nnoremap p :wincmd w:echo "Previous window." +nnoremap n :wincmd W:echo "Next window." +nnoremap o :wincmd o:echo "Only one window." + +" Move between Vim windows and Tmux panes +" - It requires the corresponding configuration into Tmux. +" - Check it at my .tmux.conf from my dotfiles repository. +" - URL: https://github.com/gerardbm/dotfiles/blob/master/tmux/.tmux.conf +" - Plugin required: https://github.com/christoomey/vim-tmux-navigator +if !has("nvim") + set =h + set =j + set =k + set =l +endif + +nnoremap :TmuxNavigateLeft +nnoremap :TmuxNavigateDown +nnoremap :TmuxNavigateUp +nnoremap :TmuxNavigateRight + +" Remove the Windows ^M - when the encodings gets messed up +noremap mmHmt:%s///ge'tzt`m + +" Close the preview window +nnoremap . :pclose + +" Scroll the preview window +if !has("nvim") + set =d + set =u +endif + +nnoremap :wincmd P5:wincmd p +nnoremap :wincmd P5:wincmd p + +"---------------------------------------------------------------- +" 10. Indentation tabs +"---------------------------------------------------------------- +" Enable autoindent & smartindent +set autoindent +set smartindent + +" Use tabs, no spaces +set noexpandtab + +" Be smart when using tabs +set smarttab + +" Tab size (in spaces) +set shiftwidth=2 +set tabstop=2 + +" Remap indentation +nnoremap >> +nnoremap << + +vnoremap >gv +vnoremap :set list!:echo g:f6msg + +" Show tabs and end-of-lines +set listchars=tab:┊\ ,trail:¬ + +"---------------------------------------------------------------- +" 11. Moving around lines +"---------------------------------------------------------------- +" Specify which commands wrap to another line +set whichwrap+=<,>,h,l + +" Many jump commands move the cursor to the start of line +set nostartofline + +" Wrap lines into the window +set wrap + +" Don't break the words +" Only works if I set nolist (F6) +set linebreak +set showbreak=↳\ + +" Stop automatic wrapping +set textwidth=0 + +" Column at 80 width +set colorcolumn=80 + +" Listings don't pause +set nomore + +" Color column +let g:f10msg = 'Toggle colorcolumn.' +nnoremap :call ToggleColorColumn():echo g:f10msg + +" Show line numbers +set number +set numberwidth=2 + +let g:f3msg = 'Toggle line numbers.' +nnoremap :set number!:echo g:f3msg + +" Set relative line numbers +set relativenumber + +let g:f4msg = 'Toggle relative line numbers.' +nnoremap :set norelativenumber!:echo g:f4msg + +" Treat long lines as break lines (useful when moving around in them) +nnoremap k (v:count == 0 ? 'gk' : 'k') +nnoremap j (v:count == 0 ? 'gj' : 'j') + +vnoremap k (v:count == 0 ? 'gk' : 'k') +vnoremap j (v:count == 0 ? 'gj' : 'j') + +nnoremap g^ +nnoremap g$ + +vnoremap g^ +vnoremap g$ + +" Toggle the cursor position start/end of the line +nnoremap ñ :call ToggleCPosition('') +vnoremap ñ :call ToggleCPosition('normal! gv') + +" Join / split lines +nnoremap J +nnoremap i + +" Duplicate a line +nnoremap cx yyP +nnoremap cv yyp + +" Folding +set foldmethod=marker + +" Return to last edit position when opening files +autocmd BufReadPost * + \ if line("'\"") > 0 && line("'\"") <= line("$") | + \ exe "normal! g`\"" | + \ endif + +" --- Readline commands --- +"---------------------------------------------------------------- +" Move the cursor to the line start +inoremap 0 + +" Move the cursor to the line end +inoremap $ + +" Moves the cursor back one character +inoremap + +" Moves the cursor forward one character +inoremap + +" Remove one character +inoremap + +" Command Mode +cnoremap +cnoremap +cnoremap +cnoremap +cnoremap +cnoremap +cnoremap +cnoremap " +cnoremap + +"---------------------------------------------------------------- +" 12. Paste mode +"---------------------------------------------------------------- +" Bracketed paste mode +" - Source: https://ttssh2.osdn.jp/manual/en/usage/tips/vim.html +if !has("nvim") + if has("patch-8.0.0238") + if &term =~ "screen" + let &t_BE = "\e[?2004h" + let &t_BD = "\e[?2004l" + exec "set t_PS=\e[200~" + exec "set t_PE=\e[201~" + endif + endif +endif + +"---------------------------------------------------------------- +" 13. Search, vimgrep and grep +"---------------------------------------------------------------- +" Highlight search results +set hlsearch + +" Makes search act like search in modern browsers +set incsearch + +" Search, wrap around the end of the buffer +set wrapscan + +" Ignore case when searching +set ignorecase + +" When searching try to be smart about cases +set smartcase + +" For regular expressions turn magic on +set magic + +" Maximum amount of memory in Kbyte used for pattern matching +set maxmempattern=1000 + +" --- Highlight --- +"---------------------------------------------------------------- +" Map to / (search) +nnoremap / +nnoremap ? + +" Highlight the word under the cursor and don't jump to next +nnoremap :let @/='\<=expand("")\>':set hlsearch + +" Highlight the selected text and don't jump to next +vnoremap :call VSetSearch():set hlsearch + +" Disable highlight +nnoremap m :noh + +" Next and prev centered +nnoremap n nzz +nnoremap N Nzz + +" Search into a Visual selection +vnoremap :call RangeSearch('/') + \ :if strlen(g:srchstr) > 0 + \ \|exec '/'.g:srchstr\|endifn +vnoremap ? :call RangeSearch('?') + \ :if strlen(g:srchstr) > 0 + \ \|exec '?'.g:srchstr\|endifN + +" --- Vimgrep and grep --- +"---------------------------------------------------------------- +" Vimgrep the highlight in the current file +nnoremap vg :vimgrep ///j % + +" Vimgrep the highlight in the current directory and subdirectories +nnoremap vf :vimgrep ///j **/*. + +autocmd QuickfixCmdPre grep,grepadd,vimgrep,vimgrepadd,helpgrep copen + +" Grep settings +set grepprg=grep\ -nHi + +" Current buffer +nnoremap vv :call GrepWrapper('grep!', '', '%') + +" Current working directory +nnoremap vn :call GrepWrapper('grep!', '-R + \ --exclude-dir={.git,.svn,.jekyll-cache,_site} + \ --exclude=LICENSE', '') + +" Current buffer (grepadd) +nnoremap vm :call GrepWrapper('grepadd!', '', '%') + +" Current arglist +nnoremap va :call GrepWrapper('grep!', '', '##') + +" Navigate between grep and vimgrep results +nnoremap n :cnextzz +nnoremap N :cprevzz + +" Jump to the results in buffers (first open window), not tabs +set switchbuf=useopen + +" --- Replace --- +"---------------------------------------------------------------- +" Replace the highlight in the current file +nnoremap r :%s////g + +" Replace the highlight in the visual selection +" Flag \%V --> Match only inside the visual selection +vnoremap r :s/\%V/\%V//g + +" Replace the highlight to all loaded buffers and arglist +nnoremap R :bufdo %s////ge + +" Replace the highlight to each valid entry in the quickfix +nnoremap Q :cdo %s////ge + +" Populate the arglist +nnoremap aa :argadd +nnoremap ad :argdelete +nnoremap an :args **/*. +nnoremap al :call DisplayArglist():argument + +"---------------------------------------------------------------- +" 14. Text edition +"---------------------------------------------------------------- +" Toggle case +nnoremap z ~ +vnoremap z y:call setreg('', ToggleCase(@"), getregtype(''))gv""Pgv +vnoremap ~ y:call setreg('', ToggleCase(@"), getregtype(''))gv""Pgv + +" Toggle and untoggle spell checking +let g:f8msg = 'Toggle spell checking.' +nnoremap :setlocal spell!:echo g:f8msg + +" Toggle spell dictionary +nnoremap :call ToggleSpelllang() + +" Move to next misspelled word +nnoremap zl ]s + +" Find the misspelled word before the cursor +nnoremap zh [s + +" Suggest correctly spelled words +nnoremap zp z= + +" Copy text into the clipboard +vnoremap y "+y + +" Paste text from the clipboard +nnoremap p "+p + +" Quickly select the text pasted from the clipboard +nnoremap gV `[v`] + +" Yank everything from the cursor to the EOL +nnoremap Y y$ + +" Yank the last pasted text automatically +vnoremap p pgvy + +" Retab the selected text +nnoremap tf :retab! +vnoremap tf :retab! + +" Isolate the current line +nnoremap o m`okO`` + +" Enter a new line Down from 'Normal Mode' +nnoremap f mao`a + +" Enter a new line Up from 'Normal Mode' +nnoremap F maO`a + +" Insert brackets faster using the ISO-ES layout +inoremap ñr [] +inoremap ñb () +inoremap ñB {} +inoremap ññ {{ }} +inoremap ñl \ +autocmd FileType html,markdown,liquid inoremap ñp {% %} +autocmd FileType html,markdown,liquid inoremap ñ- {%- -%} + +" Enter Vim's expression register (math) +inoremap ñm = + +"---------------------------------------------------------------- +" 15. Make settings +"---------------------------------------------------------------- +" Set makeprg +if !filereadable(expand('%:p:h').'/Makefile') + autocmd FileType c setlocal makeprg=gcc\ %\ &&\ ./a.out +endif + +" Go to the error line +set errorformat=%m\ in\ %f\ on\ line\ %l + +" Use the correct cursor shape via 'edit-command-line' (zle) +augroup zsh + autocmd! + if !has("nvim") + autocmd Filetype zsh silent! exec "! echo -ne '\e[2 q'" + endif +augroup end + +" Run code in a tmux window +augroup tmuxy + autocmd! + autocmd FileType javascript,lua,perl,php,python,ruby,sh + \ nnoremap ij + \ :call Tmuxy() +augroup end + +" Run code in the preview window +augroup scripty + autocmd! + autocmd FileType javascript,lua,perl,php,python,ruby,sh + \ nnoremap ii + \ :call Scripty() +augroup end + +" Work with Sqlite databases +augroup sqlite + autocmd! + autocmd FileType sql nnoremap ia + \ :call SqliteDatabase() + autocmd FileType sql nnoremap ii + \ :call SQLExec('n') + autocmd FileType sql vnoremap ii + \ :call SQLExec('v') +augroup end + +" Work with maxima (symbolic mathematics) +augroup maxima + autocmd! + autocmd BufRead,BufNewFile *.max set filetype=maxima + autocmd FileType maxima nnoremap ii + \ :call MaximaExec('n') + autocmd FileType maxima vnoremap ii + \ :call MaximaExec('v') +augroup end + +" Convert LaTeX to PDF +augroup latex + autocmd! + autocmd FileType tex nnoremap ii + \ :call Generator('.pdf', &ft) +augroup end + +" Convert markdown to PDF, HTML and EPUB +augroup markdown + autocmd! + autocmd FileType markdown nnoremap ii + \ :call Generator('.pdf', &ft) + autocmd FileType markdown nnoremap ih + \ :call Generator('.html', &ft) + autocmd FileType markdown nnoremap ij + \ :call Generator('.epub', &ft) +augroup end + +" Draw with PlantUML +augroup uml + autocmd! + autocmd FileType plantuml nnoremap ii + \ :call Generator('.png', &ft) + autocmd FileType plantuml nnoremap ij + \ :call Generator('.svg', &ft) +augroup end + +" Draw with Graphviz +augroup dot + autocmd! + autocmd FileType dot nnoremap ii + \ :call Generator('.png', &ft) +augroup end + +" Draw with Eukleides +augroup eukleides + autocmd! + autocmd BufRead,BufNewFile *.euk set filetype=eukleides + autocmd FileType eukleides nnoremap ii + \ :call Generator('.png', &ft) +augroup end + +" Draw with Asymptote +augroup asymptote + autocmd! + autocmd BufRead,BufNewFile *.asy set filetype=asy + autocmd FileType asy nnoremap ii + \ :call Generator('.png', &ft) +augroup end + +" Draw with pp3 +augroup pp3 + autocmd! + autocmd BufRead,BufNewFile *.pp3 set filetype=pp3 + autocmd FileType pp3 nnoremap ii + \ :call Generator('.png', &ft) +augroup end + +" Draw with Gnuplot +augroup gnuplot + autocmd! + autocmd BufRead,BufNewFile *.plt set filetype=gnuplot + autocmd FileType gnuplot nnoremap ii + \ :call Generator('.png', &ft) +augroup end + +" Draw with POV-Ray +augroup povray + autocmd! + autocmd FileType pov nnoremap ii + \ :call Generator('.png', &ft) +augroup end + +" Run Jekyll (liquid.markdown) +augroup liquid + autocmd! + autocmd FileType liquid.markdown,html,yaml set wildignore+=*/.jekyll-cache/*, + \*/_site/*,*/images/*,*/timg/*,*/icons/*,*/logo/*,*/where/* + autocmd FileType liquid.markdown setlocal spell spelllang=es colorcolumn=0 + autocmd FileType liquid.markdown,yaml nnoremap ii + \ :call ToggleJekyll() + autocmd FileType liquid.markdown nnoremap ij + \ :call ViewJekyllPost() +augroup end + +"---------------------------------------------------------------- +" 16. Filetype settings +"---------------------------------------------------------------- +" Delete trailing white spaces +func! s:DeleteTrailing() + exe 'normal mz' + %s/\s\+$//ge + exe 'normal `z' + echo 'Trailing white spaces have been removed.' + noh +endfunc + +nnoremap dt :call DeleteTrailing() + +" Binary +augroup binary + autocmd! + autocmd BufReadPre *.bin let &bin=1 + autocmd BufReadPost *.bin if &bin | %!xxd + autocmd BufReadPost *.bin set ft=xxd | endif + autocmd BufWritePre *.bin if &bin | %!xxd -r + autocmd BufWritePre *.bin endif + autocmd BufWritePost *.bin if &bin | %!xxd + autocmd BufWritePost *.bin set nomod | endif +augroup end + +" Mail +augroup mail + autocmd! + autocmd FileType mail setl spell + autocmd FileType mail setl spelllang=ca +augroup end + +" SQL (it requires sqlparse) +augroup sql + let g:omni_sql_no_default_maps = 1 + autocmd FileType sql nnoremap bf + \ :%!sqlformat --reindent --keywords upper --identifiers upper - + autocmd FileType sql vnoremap bf + \ :%!sqlformat --reindent --keywords upper --identifiers upper - +augroup end + +" XML (it requires tidy) +augroup xml + autocmd FileType xml nnoremap bf + \ :%!tidy -q -i -xml --show-errors 0 -wrap 0 --indent-spaces 4 +augroup end + +" MD +augroup md + autocmd FileType markdown,liquid,text,yaml set expandtab + autocmd FileType markdown,liquid,text + \ nnoremap cc :call KeywordDensity() + autocmd FileType markdown,liquid,text nnoremap cx g + autocmd FileType markdown,liquid,text vnoremap cx g + autocmd FileType markdown,liquid,text + \ nnoremap gl :call search('\v\[[^]]*]\([^)]*\)', 'W') + autocmd FileType markdown,liquid,text + \ nnoremap gh :call search('\v\[[^]]*]\([^)]*\)', 'bW') + autocmd FileType markdown,liquid,text + \ nnoremap gd :call RemoveMdLink() + autocmd FileType markdown,liquid,text + \ :command! -range Enes ,!trans en:es -brief + autocmd FileType markdown,liquid,text + \ :command! -range Esen ,!trans es:en -brief + autocmd FileType markdown,liquid,text + \ nnoremap gx :call CustomGx() +augroup end + +" CSV +augroup csv + autocmd! + autocmd BufRead,BufNewFile *.csv set filetype=csv +augroup end + +" New file headers +augroup headers + autocmd! + autocmd BufNewFile *.py 0put =\"#!/usr/bin/env python\\# + \ -*- coding: utf-8 -*-\\"|$ + autocmd BufNewFile *.rb 0put =\"#!/usr/bin/env ruby\\"|$ + autocmd BufNewFile *.pl 0put =\"#!/usr/bin/env perl\\"|$ + autocmd BufNewFile *.sh 0put =\"#!/usr/bin/env bash\\"|$ + autocmd BufNewFile *.js 0put =\"#!/usr/bin/env node\\"|$ +augroup end + +"---------------------------------------------------------------- +" 17. Helper functions +"---------------------------------------------------------------- +" Toggle Terminal +function! s:ToggleTerminal() + if bufexists('terminal') + call win_gotoid(s:winZsh) + norm! i + else + execute ':terminal' + keepalt file terminal + let s:winZsh = win_getid() + norm! i + endif +endfunction + +" Show syntax highlighting groups +function! s:SynStack() + if !exists('*synstack') + return + endif + echo map(synstack(line('.'), col('.')), 'synIDattr(v:val, "name")') +endfunction + +" Rename file +function! s:RenameFile() + let l:old_name = expand('%') + let l:new_name = input('New file name: ', expand('%'), 'file') + if l:new_name !=# '' && l:new_name !=# l:old_name + exec ':saveas ' . l:new_name + exec ':silent !rm ' . l:old_name + redraw! + endif +endfunction + +" Don't close window when deleting a buffer +function! s:CustomCloseBuffer() + let l:currentBufNum = bufnr('%') + let l:alternateBufNum = bufnr('#') + + if buflisted(l:alternateBufNum) + buffer # + else + bnext + endif + + if bufnr('%') == l:currentBufNum + new + endif + + if buflisted(l:currentBufNum) + exec ':bdelete! ' . l:currentBufNum + endif +endfunction + +" Close the last buffer if it's the last window +" - For 'quickfix' and 'nofile' +autocmd BufEnter * call CloseLastBuffer() +function! s:CloseLastBuffer() + if &buftype ==# 'quickfix' || &buftype ==# 'nofile' + if winnr('$') < 2 + quit! + endif + endif +endfunction + +" Display the arglist vertically +function! s:DisplayArglist() abort + let l:argnum = 0 + let l:lenargc = len(argc()) + for l:arg in argv() + let l:argnum += 1 + let l:filename = fnamemodify(l:arg, ':t') + let l:changed = + \ getbufinfo(bufname('^' . l:arg . '$'))[0].changed == 1 + \ ? '+' + \ : ' ' + let l:current = expand('%') ==# l:arg ? '%' : ' ' + echo printf( '%-*d %s%s %s', + \ l:lenargc, l:argnum, l:current, l:changed, l:filename) + endfor +endfunction + +" Toggle maximize window and resize windows +function! s:ToggleResize() abort + if winnr('$') > 1 + if exists('t:zoomed') && t:zoomed + execute t:zoom_winrestcmd + let t:zoomed = 0 + echo 'Windows resized.' + else + let t:zoom_winrestcmd = winrestcmd() + resize + vertical resize + let t:zoomed = 1 + echo 'Window maximized.' + endif + endif +endfunction +autocmd VimEnter * autocmd WinEnter * let t:zoomed = 0 + +" Search into a Visual selection +function! s:RangeSearch(direction) + call inputsave() + let g:srchstr = input(a:direction) + call inputrestore() + if strlen(g:srchstr) > 0 + let g:srchstr = g:srchstr. + \ '\%>'.(line("'<")-1).'l'. + \ '\%<'.(line("'>")+1).'l' + else + let g:srchstr = '' + endif +endfunction + +" Highlight the selected text (visual mode) +" Source: https://github.com/nelstrom/vim-visual-star-search +" (You can install it as a plugin) +function! s:VSetSearch() + let l:temp = @@ + norm! gvy + let @/ = '\V' . substitute(escape(@@, '\'), '\n', '\\n', 'g') + let @@ = l:temp +endfunction + +" Count grep matches +function! QFCounter() abort + let l:results = len(getqflist()) + if l:results > 0 + copen + else + cclose + endif + echo 'Found ' . l:results . ' matches.' +endfunction + +" Grep wrapper +function! s:GrepWrapper(cmd, dir, scope) abort + cclose + let l:pattern = substitute(@/, '\\V', '', '') + let l:pattern = substitute(pattern, '\\<', '', '') + let l:pattern = substitute(pattern, '\\>', '', '') + let l:pattern = escape(pattern, '"') + let l:pattern = escape(pattern, '%') + let l:pattern = escape(pattern, '#') + silent execute a:cmd . ' ' . a:dir . ' "' . l:pattern . '" ' . a:scope + redraw! + set hlsearch + call QFCounter() +endfunction + +" Toggle case +function! ToggleCase(str) + if a:str ==# toupper(a:str) + let l:result = tolower(a:str) + elseif a:str ==# tolower(a:str) + let l:result = substitute(a:str,'\(\<\w\+\>\)', '\u\1', 'g') + else + let l:result = toupper(a:str) + endif + return l:result +endfunction + +" Toggle spell dictionary +function! ToggleSpelllang() + if (&spelllang =~# '^en') + set spelllang=ca + echo 'Català' + elseif (&spelllang =~# '^ca') + set spelllang=es + echo 'Castellano' + elseif (&spelllang =~# '^es') + set spelllang=en_us,en_gb + echo 'English' + endif + set spelllang? +endfunction + +" New :windo command to return to the last current window +" - Default :windo cycles through all the open windows +" - Use 'Windo' instead of 'windo' to avoid it +command! -nargs=+ -complete=command Windo + \ let s:currentWindow = winnr() | + \ execute 'windo ' | + \ exe s:currentWindow . 'wincmd w' + +" Toggle colorcolumn +function! s:ToggleColorColumn() + if !exists('s:color_column_old') || s:color_column_old == 0 + let s:color_column_old = &colorcolumn + Windo let &colorcolumn = 0 + else + Windo let &colorcolumn = s:color_column_old + let s:color_column_old = 0 + endif +endfunction + +" Toggle the cursor position start/end +function! s:ToggleCPosition(mode) + execute a:mode + if !exists('s:togglecp') + let s:togglecp = 0 + endif + if col('.') >= col('$') - 1 + let s:togglecp = 1 + norm! ^ + else + let s:togglecp = 0 + norm! $ + endif +endfunction + +" Toggle GitGutterPreviewHunk +function! s:ToggleGGPrev() + if getwinvar(winnr('#'), '&pvw') == 1 + \ && exists('s:curr') && line(".") == s:curr + \ || gitgutter#hunk#in_hunk(line(".")) == 0 + pclose + else + GitGutterPreviewHunk + let s:curr = line(".") + endif +endfunction + +" Switch CWD to git root directory +function! s:GitRoot() abort + execute ':lcd %:p:h' + let l:isgit = system('git rev-parse --is-inside-work-tree 2>/dev/null') + if v:shell_error == 0 + let l:path = system('git rev-parse --show-toplevel 2>/dev/null') + execute ':lcd ' . l:path + execute ':pwd' + else + echo "You're not in a git directory." + endif +endfunction + +" Toggle Gstatus window +function! s:ToggleGstatus() abort + for l:winnr in range(1, winnr('$')) + if !empty(getwinvar(l:winnr, 'fugitive_status')) + exe l:winnr 'close' + return + endif + endfor + keepalt Git +endfunction + +" Better toggle for NERDTree +function! s:ToggleNERDTree() abort + if bufname('%') != "" + if exists("g:NERDTree") && g:NERDTree.IsOpen() + if &filetype ==# 'nerdtree' + execute ':NERDTreeClose' + else + execute ':NERDTreeFocus' + endif + else + execute ':NERDTreeFind' + endif + else + execute ':NERDTreeToggleVCS' + endif +endfunction + +" Get Tagbar buffer name +function! s:TagbarBufName() abort + if !exists('t:tagbar_buf_name') + let s:buffer_seqno += 1 + let t:tagbar_buf_name = '__Tagbar__.' . s:buffer_seqno + endif + return t:tagbar_buf_name +endfunction + +" Better toggle for Tagbar +function! s:ToggleTagbar() abort + let tagbarwinnr = bufwinnr(s:TagbarBufName()) + if tagbarwinnr != -1 + if &modifiable + execute tagbarwinnr 'wincmd w' + else + execute ':TagbarClose' + endif + else + execute ':TagbarOpen' + endif +endfunction + +" Keyword density checker +function! s:KeywordDensity() abort + silent update + + " Words count + " > Strip the front matter, comments, HTML tags and count the words + let s:sf = " | sed '1 { /^---/ { :a N; /\\n---/! ba; d } }'" + let s:sc = " | sed 's/{% comment.*endcomment %}//g'" + let s:sh = " | sed 's/<[^>]*.//g'" + let s:wc = " | wc -w" + let s:get_words = system("cat " . expand('%') . s:sf. s:sc . s:sh . s:wc) + let s:int_words = str2nr(s:get_words) + let s:str_words = string(s:int_words) + + " Highlight count + let s:match_count = "" + redir => s:match_count + silent exec '%s/' . @/ . '//gne' + redir END + + if ! empty(s:match_count) + let s:str_keys = split(s:match_count)[0] + let s:flt_keys = str2float(s:str_keys) + + " Density + let s:flt_dens = (s:flt_keys/s:int_words)*100 + let s:str_dens = string(s:flt_dens) + + echo '> ' . s:str_keys . ' of ' . s:str_words . ' (' . s:str_dens . '%)' + else + echo '> 0 of ' . s:str_words . ' (0%, pattern not found)' + endif +endfunction + +" Remove markdown link +function! s:RemoveMdLink() abort + let [l, c] = searchpos('\v\[[^]]*]\([^)]*\)', 'ncW') + if l > 0 && c > 0 + if getline(".")[col(".")-1] ==# "[" + norm! xf]vf)d + else + call cursor(l, c) + endif + endif +endfunction + +" Custom gx to preview images in markdown +function! s:CustomGx() + let l:src = expand("") + execute "silent !feh " . substitute(l:src, '^/', '', 'g') . " &" + redraw! +endfunction + +function! CheckBackspace() abort + let col = col('.') - 1 + return !col || getline('.')[col - 1] =~# '\s' +endfunction + +"---------------------------------------------------------------- +" 18. External tools integration +"---------------------------------------------------------------- +" Run code into a tmux window +function! s:Tmuxy() abort + if exists('$TMUX') + update + let s:runner = Runners() + let s:cmdk = 'tmux kill-window -t run' + let s:cmdn = 'tmux new-window -n run' + let s:cmds = " '" . s:runner . " " . expand("%:p") . " ; read'" + call system(s:cmdk) + call system(s:cmdn . s:cmds) + else + echo 'Tmux is not running.' + endif +endfunction + +" Run code in the preview window +function! s:Scripty() abort + update + let s:runner = Runners() + let s:cmd = s:runner . " " . expand("%:p") + call Commander(s:cmd) +endfunction + +" Define the runners +function! s:Runners() abort + if &filetype =~# 'javascript' + let s:run = 'node' + elseif &filetype =~# 'lua' + let s:run = 'lua' + elseif &filetype =~# 'perl' + let s:run = 'perl' + elseif &filetype =~# 'php' + let s:run = 'php' + elseif &filetype =~# 'python' + let s:run = 'python3' + elseif &filetype =~# 'ruby' + let s:run = 'ruby' + elseif &filetype =~# 'sh' + let s:run = 'bash' + else + let s:run = 'empty' + endif + return s:run +endfunction + +" Generator function +function! s:Generator(ext, ft) abort + update + let l:inp = expand('%') + let l:out = expand('%:r') . a:ext + if a:ft ==# 'tex' + let l:cmd = system('pdflatex ' . l:inp) + elseif a:ft ==# 'markdown' + if a:ext ==# '.html' + let l:opt = '--mathjax ' + elseif a:ext ==# '.epub' + let l:opt = '-t epub2 --webtex ' + elseif a:ext ==# '.pdf' + let l:opt = '-V fontsize=12pt + \ -V papersize=a4 + \ -V geometry:margin=2.5cm ' + endif + let l:cmd = system('pandoc -s ' . l:opt . l:inp . ' -o ' . l:out) + elseif a:ft ==# 'plantuml' + if a:ext ==# '.png' + let l:cmd = system('plantuml ' . l:inp) + elseif a:ext ==# '.svg' + let l:cmd = system('plantuml -tsvg ' . l:inp) + endif + elseif a:ft ==# 'dot' + let l:cmd = system('dot -Tpng ' . l:inp . ' -o ' . l:out) + elseif a:ft ==# 'eukleides' + let l:eps = expand('%:r') . '.eps' + let l:cmd = system('eukleides ' . l:inp) + elseif a:ft ==# 'asy' + let l:eps = expand('%:r') . '.eps' + let l:cmd = system('asy ' . l:inp) + elseif a:ft ==# 'pp3' + let l:eps = expand('%:r') . '.eps' + let l:cmd = system('pp3 ' . l:inp) + elseif a:ft ==# 'gnuplot' + let l:opt = ' -e "set terminal png; set output ''' . l:out . '''" ' + let l:cmd = system('gnuplot' . l:opt . l:inp) + elseif a:ft ==# 'pov' + let l:cmd = system('povray -D ' . l:inp) + endif + if v:shell_error == 0 + pclose + if a:ft =~# '\(eukleides\|asy\|pp3\)' + call EPS2PNG(l:eps, l:out) + endif + call Previewer(l:out, a:ext) + else + call WinPreview() + exec '0put =l:cmd' + call ResizeWinPreview() + endif +endfunction + +" Convert from EPS to PNG +function! s:EPS2PNG(eps, out) abort + let l:opt_bef = ' -density 150 ' + let l:opt_aft = ' -flatten -alpha off -colorspace hsl ' + call system('convert' . l:opt_bef . a:eps . l:opt_aft . a:out) +endfunction + +" Preview EPUB, HTML & PNG with mupdf +" Preview PDF with zathura +function! s:Previewer(out, ft) abort + if a:ft ==# '.pdf' + let l:tool = 'zathura' + elseif a:ft ==# '.svg' + let l:tool = 'geeqie' " 'eog' is also a good option + else + let l:tool = 'mupdf' + endif + let l:dev = ' 2>/dev/null' + let l:checkps = system('ps -ef + \ | grep -v grep + \ | grep '. l:tool .' + \ | grep -o ' . a:out . l:dev) + if l:checkps ==# '' + call system(l:tool . ' ' . a:out . ' &') + else + if l:tool ==# 'mupdf' + call system('pkill -HUP mupdf') + endif + endif +endfunction + +" Configure a sqlite database +function! s:SqliteDatabase() abort + let t:path = input('Database: ') +endfunction + +" Execute SQL queries +function! s:SQLExec(opt) abort + if a:opt ==# 'n' + silent norm! yy + elseif a:opt ==# 'v' + silent norm! gvy + endif + if !exists('t:path') + call SqliteDatabase() + endif + if filereadable(t:path) + let t:sql = @ + let t:sql = substitute(t:sql, '\n', ' ', 'g') + let t:format = " | column -t -s '|'" + if t:sql =~? '^select' + let t:cmd = t:path . ' "' . escape(t:sql, '"') . '"' . t:format + else + let t:cmd = t:path . ' "' . escape(t:sql, '"') . '"' + endif + let s:cmd = "sqlite3 -list -batch " . t:cmd + call Commander(s:cmd) + else + echo "\nThis database does not exist!" + endif +endfunction + +" Execute Maxima instructions +function! s:MaximaExec(opt) abort + if a:opt ==# 'n' + silent norm! yy + elseif a:opt ==# 'v' + silent norm! gvy + endif + let b:equ = @ + let b:equ = substitute(b:equ, '\n', ' ', 'g') + let b:equ = substitute(b:equ, '\s$', '', 'g') + let b:equ = substitute(b:equ, '%', '\\%', 'g') + if b:equ !~# ';$' + let b:equ = substitute(b:equ, '$', ';', 'g') + endif + let s:cmd = 'maxima --very-quiet --batch-string "' . b:equ . '"' + call Commander(s:cmd) +endfunction + +" Window previewer +function! s:WinPreview() abort + silent! wincmd P + if !&previewwindow + exec 'new' + setlocal previewwindow + setlocal buftype=nowrite bufhidden=wipe + setlocal nobuflisted noswapfile nowrap + nnoremap q :pclose + endif + exec '%delete' +endfunction + +" Commander +function! s:Commander(cmd) abort + call WinPreview() + exec '0read !' . a:cmd + call ResizeWinPreview() +endfunction + +" Resize the preview window +function! s:ResizeWinPreview() abort + exec '$d' + let s:size = line('$') + if s:size < 10 + exec 'resize ' . line('$') + else + exec 'resize 10' + endif + norm! gg + wincmd p +endfunction + +command! -nargs=1 Commander call Commander() + +" Toggle Jekyll server in the background +function! s:ToggleJekyll() abort + call system('lsof -i :4000 | grep -i listen') + if v:shell_error + silent exec "!(bundle exec jekyll serve -q -l -I &) > /dev/null" + call system("touch /tmp/jekyll.ps") + call system("notify-send -t 2 'Executing Jekyll server...'") + else + silent exec "!(pkill -f jekyll &) > /dev/null" + call system("rm -f /tmp/jekyll.ps") + call system("notify-send -t 2 'Jekyll server was stoped!'") + endif + redraw! +endfunction + +" Preview the current post in the web broswer +function! s:ViewJekyllPost() abort + silent update + let s:grep = expand("grep '^permalink: ' ") + let s:file = expand('%') + let s:sed = expand(" | sed 's/permalink: //g'") + let s:permalink = system(s:grep . s:file . s:sed) + call system("google-chrome http://localhost:4000" . s:permalink) +endfunction + +" Shell output to the buffer +command! -nargs=1 SH :r ! + +hi Normal guibg=NONE ctermbg=NONE