diff --git a/fix.sh b/fix.sh index 6b69892..2b4e8a0 100755 --- a/fix.sh +++ b/fix.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # Script for fixing hardcoded icons. Written and maintained on GitHub # at https://github.com/Foggalong/hardcode-fixer - addtions welcome! @@ -10,185 +10,622 @@ # a copy of the GNU General Public License along with this program. # If not, see . -# Version info -date=201709040 # [year][month][date][extra] - -# Locations -git_locate="https://raw.githubusercontent.com/Foggalong/hardcode-fixer/master" -username=${SUDO_USER:-$USER} -userhome="/home/$username" -app_dirs=("$userhome/.local/share/applications/" - "$userhome/.local/share/applications/kde4/" - "$(sudo -u $username xdg-user-dir DESKTOP)/" - "/usr/share/applications/" - "/usr/share/applications/kde4/" - "/usr/local/share/applications/" - "/usr/local/share/applications/kde4/") -local_apps="$userhome/.local/share/applications/" -local_icon="$userhome/.local/share/icons/hicolor/48x48/apps/" -steam_icon="/usr/share/icons/hicolor/48x48/apps/steam.png" - - -# Allows timeout when launched via 'Run in Terminal' -function gerror() { sleep 3; exit 1; } - - -# Fix Launcher -function fix_launch() { - launcher=$1 - name=$2 - icon=$3 - type=$4 - - # Check if already fixed, if not create marked launcher copy - local_version="$local_apps$name.desktop" - if [ -f "$local_version" ]; then - line=$(head -n 1 $local_version) - if [[ $line == "# HC"* ]]; then - return - else - cp "$local_version" "$local_version.old" - sed -i '1i# HC:Local' "$local_version" - fi - else - cp "$launcher" "$local_version" - sed -i '1i# HC:Global' "$local_version" - fi - - echo -n "$type: Fixing $name..." - - # Making copy of needed icon and determining new icon name - if [[ $type == "H" ]]; then - new_icon=$(echo ${name} | sed -e 's/\ /_/' ) # | sed -e 's/\./_/' ) - ext=$(echo ${icon} | sed -e 's/.*\.//' ) - cp $icon $local_icon$new_icon.$ext - elif [[ $type == "S" ]]; then - exec=$(grep '^Exec=' ${file} | sed -e 's/.*Exec=//' ) - new_icon=steam_icon_$(echo $exec | sed -e 's/steam\ steam:\/\/rungameid\/*//' ) - cp $steam_icon "$local_icon$new_icon.png" - fi - - sed -i "s|Icon=${icon}.*|Icon=${new_icon}|g" $local_version - echo " done" -} - - -# Deals with the flags -if [ -z "$1" ]; then - mode="fix" -else - case $1 in - -r|--revert) - echo "This will undo all changes previously made." - while true; do - read -r -p "Are you sure you want to continue? " answer - case $answer in - [Yy]* ) mode="revert"; break;; - [Nn]* ) exit;; - * ) echo "Please answer [Y/y]es or [N/n]o.";; - esac - done;; - -h|--help) - echo -e \ - "Usage: ./$(basename -- $0) [OPTION]\n" \ - "\rFixes hardcoded icons of installed applications.\n\n" \ - "\rCurrently supported options:\n" \ - "\r -r, --revert \t\t Reverts any changes made.\n" \ - "\r -h, --help \t\t Displays this help menu.\n" \ - "\r -v, --version \t Displays program version.\n" - exit 0 ;; - -v|--version) - echo -e "$(basename -- $0) $date\n" - exit 0 ;; - *) - echo -e "$(basename -- $0): invalid option -- '$1'" - echo -e "Try '$(basename -- $0) --help' for more information." - gerror - esac +if test -z "$BASH_VERSION"; then + printf "Error: this script only works in bash.\n" >&2 + exit 1 fi - -# TODO this entire 40+ LOC is just for checking for updates. should this still be here? -# Verifies if 'curl' is installed -if ! type "curl" >> /dev/null 2>&1; then - echo -e \ - "$0: This script requires 'curl' to be installed\n" \ - "\rto fetch the required files and check for updates.\n" \ - "\rPlease install it and rerun this script." - gerror +if [ "${BASH_VERSINFO[0]}" -lt 4 ]; then + printf "Error: this script requires bash version >= 4\n" >&2 + exit 1 fi -# Checks for having internet access -if eval "curl -sk https://github.com/" >> /dev/null 2>&1; then - : # pass -else - echo -e \ - "No internet connection available. This script\n" \ - "\rrequires internet access to connect to GitHub\n" \ - "\rto check for updates and download 'to-fix' info." - gerror -fi +set -o errexit +set -o noclobber +set -o pipefail -# Check for newer version of fix.sh -new_date=$(curl -sk "${git_locate}"/fix.sh | grep "date=[0-9]\{9\}" | sed "s/[^0-9]//g") -if [ "$date" -lt "$new_date" ]; then - echo -e \ - "You're running an out of date version of\n" \ - "\rthe script. Please download the latest\n" \ - "\rverison from the GitHub page or update\n" \ - "\rvia your package manager. If you continue\n" \ - "\rwithout updating you may run into problems." - while true; do - read -r -p "Would you like to [e]xit, or [c]ontinue?" answer - case $answer in - [Ee]* ) exit;; - [Cc]* ) break;; - * ) echo "Please answer [e]xit or [c]ontinue";; - esac - done -fi +shopt -s globstar # enable **/*.deskop path expansion +unset GREP_OPTIONS # avoid mess up +readonly SCRIPT_NAME="$(basename -- "$0")" +readonly SCRIPT_DIR="$(dirname -- "$0")" +readonly -a ARGS=( "$@" ) -if [ "$mode" == "fix" ]; then - # Iterate over all the launcher locations - for location in ${app_dirs[@]}; do - # Iterate over the files in those locations - for file in ${location}*; do - # Check if the file is a launcher - if [[ ${file} == *.desktop ]]; then - name=$(echo ${file} | sed -e 's/.*\///' | sed -e 's/\.desktop//' ) - icon=$(grep '^Icon=' ${file} | sed -e 's/.*Icon=//' ) - # Check for signs of hardcoding - if [[ $icon == *"/"* ]]; then - # What to do if the icon line is standard hardcoded - fix_launch ${file} ${name} ${icon} "H" - elif [[ $icon == "steam" ]] && [[ ${name} != "steam" ]]; then - # What to do if it's using the generic Steam icon - fix_launch ${file} ${name} ${icon} "S" - # elif [[ $icon = *"."* ]]; then - # # What to do if it's extension hardcoded - # fix_launch ${file} ${name} ${icon} "E" - fi - fi - done - done -elif [ "$mode" == "revert" ]; then - for file in ${local_apps}*; do - if [[ ${file} == *.desktop ]]; then - # Check if launcher is product of hc-fix - line=$(head -n 1 $file) - name=$(echo ${file} | sed -e 's/.*\///' | sed -e 's/\.desktop//' ) - if [[ $line == "# HC:"* ]]; then - echo -n "Reverting $name..." - icon=$(grep '^Icon=' ${file} | sed -e 's/.*Icon=//' ) - if [[ $line == "# HC:Global" ]]; then - rm $file - elif [[ $line == "# HC:Local" ]]; then - mv -f "$file.old" $file - fi - rm $local_icon$icon.* - echo " done" - fi - fi - done +readonly PROGNAME="hardcode-fixer" +declare -i VERSION=201711130 # [year][month][date][extra] +# date=999999990 # deprecate the previous version + +# Import XDG user dirs variables +if [ -f "${XDG_CONFIG_HOME:-$HOME/.config}/user-dirs.dirs" ]; then + # shellcheck disable=SC1090 + source "${XDG_CONFIG_HOME:-$HOME/.config}/user-dirs.dirs" fi + +# Get data directories from XDG_DATA_DIRS variable and +# convert colon-separated list into bash array +IFS=: read -ra DATA_DIRS <<< "${XDG_DATA_DIRS:-/usr/local/share:/usr/share}" + +UPSTREAM_URL="https://raw.githubusercontent.com/Foggalong/hardcode-fixer/master" +LOCAL_APPS_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/applications" +LOCAL_ICONS_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/icons" + +# default values of options +declare -i NO_DOWNLOAD="${NO_DOWNLOAD:-0}" +declare -i VERBOSE="${VERBOSE:-0}" + +msg() { + printf "%s: %b\n" "$PROGNAME" "$*" >&2 +} + +verbose() { + [ "$VERBOSE" -eq 1 ] || return 0 + msg "INFO:" "$*" +} + +warn() { + msg "WARNING:" "$*" +} + +err() { + msg "ERROR:" "$*" +} + +fatal() { + msg "FATAL:" "$*" + exit 1 +} + +_is_hardcoded() { + # returns true if Icon contains a path + local desktop_file="$1" + LANG=C grep -q '^Icon=.*/.*' -- "$desktop_file" +} + +_is_hardcoded_steam_app() { + # returns true if it's a Steam launcher and Icon equals 'steam' + local desktop_file="$1" + local icon_name + + if LANG=C grep -q 'steam://run' -- "$desktop_file"; then + icon_name="$(get_icon_name "$desktop_file")" + if [ "$icon_name" = "steam" ]; then + return 0 + fi + fi + + return 1 +} + +_is_update_available() { + # returns true if the upstream version is greater than current + local -i upstream_version + + upstream_version="$(get_upstream_version)" + + if [ "$VERSION" -lt "$upstream_version" ]; then + return 0 + fi + + return 1 +} + +_has_marker() { + # returns true if desktop file has a marker + local desktop_file="$1" + LANG=C grep -q '^X-HardcodeFixer-Marker=' -- "$desktop_file" +} + +get_app_name() { + local desktop_file="$1" + awk -F= '/^Name/ { print $2; exit }' "$desktop_file" +} + +get_icon_name() { + local desktop_file="$1" + awk -F= '/^Icon/ { print $2; exit }' "$desktop_file" +} + +get_steam_app_id() { + # returns id of the Steam app + local desktop_file="$1" + sed -n '/^Exec/ s/.*\/\([0-9]\+\)/\1/p' "$desktop_file" +} + +get_steam_icon_name() { + # returns name of the Steam icon (steam_icon_ID) + local desktop_file="$1" + printf 'steam_icon_%s' "$(get_steam_app_id "$desktop_file")" +} + +set_icon_name() { + # changes an icon for the desktop entry + local desktop_file="$1" + local icon_name="$2" + + sed -i -e "/^Icon=/c \ + Icon=${icon_name} + " "$desktop_file" +} + +get_marker_value() { + local desktop_file="$1" + awk -F= '/^X-HardcodeFixer-Marker/ { print $2; exit }' "$desktop_file" +} + +set_marker_value() { + local desktop_file="$1" + local marker_value="$2" + + # add after Icon + sed -i -e "/^Icon=/a \ + X-HardcodeFixer-Marker=${marker_value} + " "$desktop_file" +} + +icon_lookup() { + # looks for icon in dirs in the list and returns absolute path to the icon + local icon_name="$1" + local data_dir icons_dir icon_path + + for data_dir in \ + "${DATA_DIRS[@]}" \ + "${XDG_DATA_HOME:-$HOME/.local/share}" + do + for icons_dir in \ + "$data_dir/icons/hicolor/48x48/apps" \ + "$data_dir/icons/hicolor" \ + "$data_dir/icons" \ + "$data_dir/pixmaps" + do + for icon_path in "$icons_dir/$icon_name".*; do + [ -f "$icon_path" ] || continue + printf '%s' "$icon_path" + return 0 # only the first match + done + done + done +} + +get_icon_path() { + # returns absolute path to the icon + local desktop_file="$1" + local icon_value icon_path + + icon_value="$(get_icon_name "$desktop_file")" + + if [ "${icon_value:0:1}" = "/" ]; then + # it's absolute path + icon_path="$icon_value" + else + icon_path="$(icon_lookup "$icon_value")" + fi + + printf '%s' "$icon_path" +} + +get_upstream_version() { + download_file "$UPSTREAM_URL/fix.sh" 2> /dev/null \ + | LANG=C grep -o 'VERSION=[0-9]\+' \ + | head -1 \ + | tr -cd '[:digit:]' +} + +get_from_db() { + # returns icon name if found it + local desktop_file="$1" + local app_name + + [ -f "$DB_FILE" ] || return 1 + + app_name="$(get_app_name "$desktop_file")" + + awk -F, -v app_name="$app_name" ' + BEGIN { IGNORECASE = 1; } + $1 == app_name { + print $4 + exit + } + ' "$DB_FILE" +} + +copy_icon_file() { + local icon_path="$1" + local icon_name="$2" + local icon_ext="${icon_path##*.}" + + if [ ! -f "$icon_path" ]; then + warn "Failed to copy '$icon_name' icon." \ + "File '$icon_path' does not exist." + return 1 + fi + + mkdir -p "$LOCAL_ICONS_DIR" + + case "$icon_ext" in + png|svg|svgz|xpm) + cp -f "$icon_path" "$LOCAL_ICONS_DIR/${icon_name}.${icon_ext}" + ;; + gif|ico|jpg|jpeg) + if ! command -v convert > /dev/null 2>&1; then + warn "imagemagick is not installed." \ + "Icon '${icon_name}.${icon_ext}' cannot be converted." + return 1 + fi + + verbose "Converting '${icon_name}.${icon_ext}' to" \ + "'${icon_name}.png' ..." + convert "$icon_path" -alpha on -background none -thumbnail 48x48 \ + -flatten "$LOCAL_ICONS_DIR/${icon_name}.png" + ;; + *) + warn "'${icon_name}.${icon_ext}' has invalid icon format." + return 1 + esac +} + +download_file() { + local url="$1" + local file="${2:--}" # it's not a typo, output to stdout by default + local cmd + local -i exit_code=0 + + if command -v wget > /dev/null 2>&1; then + wget --no-check-certificate -q -O "$file" "$url" \ + || cmd="wget" exit_code="$?" + elif command -v curl > /dev/null 2>&1; then + curl -sk -o "$file" "$url" \ + || cmd="curl" exit_code="$?" + else + fatal "Fail to download '$url'.\n" \ + "\r This script requires 'wget' to be installed\n" \ + "\r to fetch the required files and check for updates.\n" \ + "\r Please install it and rerun this script." + fi + + if [ "$exit_code" -ne 0 ]; then + err "Fail to download '$url' ($cmd returns exit code $exit_code)." + fi + + return "$exit_code" +} + +translate_from_app_name() { + # converts app name to icon name + local desktop_file="$1" + + # 1. remove text between parentheses + # 2. replace spaces with hyphens + # 3. replace two or more hyphens by one + # 4. to lowercase + # 5. delete invalid characters + get_app_name "$desktop_file" \ + | sed \ + -e 's/[ ]([^)]\+)//g' \ + -e 's/[ ]/-/g' \ + -e 's/-\+/-/g' \ + | tr '[:upper:]' '[:lower:]' \ + | tr -cd '[:alnum:]-_' +} + +backup_desktop_file() { + local desktop_file="$1" + local dir_name base_name backup_file + + dir_name="$(dirname "$desktop_file")" + base_name="$(basename "$desktop_file")" + backup_file="${dir_name}/.${base_name}.orig" + + if [ -f "$backup_file" ]; then + err "Backup file already exists." + return 1 + fi + + cp "$desktop_file" "$backup_file" +} + +restore_desktop_file() { + local desktop_file="$1" + local dir_name base_name backup_file + + dir_name="$(dirname "$desktop_file")" + base_name="$(basename "$desktop_file")" + backup_file="${dir_name}/.${base_name}.orig" + + if [ -f "$backup_file" ]; then + mv -f "$backup_file" "$desktop_file" + else + err "Fail to find the backup file." + return 1 + fi +} + +fix_hardcoded_app() { + local desktop_file="$1" + local method="$2" + local app_name icon_path new_icon_name desktop_file_name local_desktop_file + + app_name="$(get_app_name "$desktop_file")" + icon_path="$(get_icon_path "$desktop_file")" + new_icon_name="$(get_from_db "$desktop_file")" + + case "$method" in + global) + desktop_file_name="$(basename "$desktop_file")" + local_desktop_file="$LOCAL_APPS_DIR/$desktop_file_name" + + if [ -e "$local_desktop_file" ]; then + verbose "'$app_name' already exists in local apps. Skipping." + return 1 + fi + + mkdir -p "$LOCAL_APPS_DIR" + cp "$desktop_file" "$local_desktop_file" + + desktop_file="$local_desktop_file" + ;; + local) + backup_desktop_file "$desktop_file" + ;; + steam) + backup_desktop_file "$desktop_file" + new_icon_name="$(get_steam_icon_name "$desktop_file")" + ;; + *) + err "illegal method -- '$method'" + return 1 + esac + + if [ -z "$new_icon_name" ]; then + new_icon_name="$(translate_from_app_name "$desktop_file")" + fi + + msg "Fixing '$app_name' [$method] ..." + + set_icon_name "$desktop_file" "$new_icon_name" + set_marker_value "$desktop_file" "$method" + copy_icon_file "$icon_path" "$new_icon_name" +} + +apply() { + local data_dir desktop_file + + # do not download if $DB_FILE already set + if [ -z "$DB_FILE" ]; then + DB_FILE="${XDG_CACHE_HOME:-$HOME/.cache}/${PROGNAME}_db.csv" + else + verbose "Using '$DB_FILE' file." + NO_DOWNLOAD=1 + fi + + if [ "$NO_DOWNLOAD" -eq 0 ]; then + verbose "Checking for $PROGNAME update ..." + if _is_update_available; then + cat >&2 <<- EOF + + You're running an out of date version of the script. + Please download the latest verison from the GitHub page + or update via your package manager. If you continue + without updating you may run into problems. + + Press [ENTER] to continue or Ctrl-c to exit + EOF + + # Wait for user to read the message + read -r < /dev/tty # don't read from stdin + fi + + msg "Downloading DB ..." + download_file "$UPSTREAM_URL/tofix.csv" "${DB_FILE}.new" \ + || warn "Will use the last saved database." + + # Rename the file if download is successful + if [ -f "${DB_FILE}.new" ] && [ -s "${DB_FILE}.new" ]; then + mv -f "${DB_FILE}.new" "$DB_FILE" + else + rm -f "${DB_FILE}.new" + fi + fi + + # exit if DB_FILE not exists or empty + if [ ! -f "$DB_FILE" ] || [ ! -s "$DB_FILE" ]; then + fatal "Database '$DB_FILE' not exists or empty." + fi + + for desktop_file in \ + "${XDG_DATA_HOME:-$HOME/.local/share}"/applications/*.desktop \ + "${XDG_DESKTOP_DIR:-$HOME/Desktop}"/**/*.desktop + do + [ -f "$desktop_file" ] || continue + + if _is_hardcoded "$desktop_file"; then + fix_hardcoded_app "$desktop_file" "local" || continue + elif _is_hardcoded_steam_app "$desktop_file"; then + fix_hardcoded_app "$desktop_file" "steam" || continue + fi + done + + for data_dir in "${DATA_DIRS[@]}"; do + for desktop_file in "$data_dir"/applications/**/*.desktop; do + [ -f "$desktop_file" ] || continue + + if _is_hardcoded "$desktop_file"; then + fix_hardcoded_app "$desktop_file" "global" || continue + fi + done + done + + msg "${FUNCNAME[0]}: Done!" +} + +revert() { + local app_name desktop_file icon_ext icon_name marker_value + + for desktop_file in \ + "$LOCAL_APPS_DIR"/*.desktop \ + "${XDG_DESKTOP_DIR:-$HOME/Desktop}"/**/*.desktop + do + [ -f "$desktop_file" ] || continue + + if _has_marker "$desktop_file"; then + app_name="$(get_app_name "$desktop_file")" + icon_name="$(get_icon_name "$desktop_file")" + marker_value="$(get_marker_value "$desktop_file")" + + msg "Reverting '$app_name' ..." + + case "$marker_value" in + global) + rm -f -- "$desktop_file" + ;; + local|steam) + restore_desktop_file "$desktop_file" || continue + ;; + *) + err "invalid marker value -- '$marker_value'" + continue + esac + + for icon_ext in png svg svgz xpm; do + if [ -f "$LOCAL_ICONS_DIR/$icon_name.$icon_ext" ]; then + verbose "Removing '$icon_name.$icon_ext' ..." + rm -- "$LOCAL_ICONS_DIR/$icon_name.$icon_ext" + break + fi + done + fi + done + + msg "${FUNCNAME[0]}: Done!" +} + +parse_opts() { + local opt cmd + local -a opts=() + local -a cmds=() + + usage() { + local exit_code="${1:-0}" + + cat >&2 <<- EOF + usage: $SCRIPT_NAME [command ...] [options] + + commands: + -A, --apply fixes hardcoded icons of installed applications + -R, --revert reverts any changes made + -V, --version print $PROGNAME version and exit + -h, --help show this help + + options: + -f, --csv-file read from the instead of downloading + -n, --no-download do not check for any updates + -v, --verbose be verbose + + Long commands without double '--' are also allowed. + EOF + + exit "$exit_code" + } + + # Translate --gnu-long-options and commands to -g (short options) + for opt; do + case "$opt" in + --apply|apply) opts+=( -A ) ;; + --revert|revert) opts+=( -R ) ;; + --version|version) opts+=( -V ) ;; + --csv-file) opts+=( -f ) ;; + --help|help) opts+=( -h ) ;; + --no-download) opts+=( -n ) ;; + --verbose) opts+=( -v ) ;; + --[0-9a-Z]*) + err "illegal option -- '$opt'" + usage 128 + ;; + *) opts+=( "$opt" ) + esac + done + + while getopts ":ARVf:hnv" opt "${opts[@]}"; do + case "$opt" in + A ) cmds+=( "apply" ) ;; + R ) cmds+=( "revert" ) ;; + V ) cmds+=( "version" ) ;; + f ) DB_FILE="$OPTARG" ;; + h ) usage 0 ;; + n ) NO_DOWNLOAD=1 ;; + v ) VERBOSE=1 ;; + : ) err "option requires an argument -- '-$OPTARG'" + usage 128 + ;; + \?) err "illegal option -- '-$OPTARG'" + usage 128 + ;; + esac + done + + for cmd in "${cmds[@]}"; do + case "$cmd" in + apply) apply ;; + revert) revert ;; + version) + printf "%s (version %s)\n" "$PROGNAME" "$VERSION" + if _is_update_available; then + msg "update is available." + fi + exit 0 + ;; + esac + done + + # display interactive menu, if no commands were passed + if [ "${#cmds[@]}" -eq 0 ]; then + show_menu + fi +} + +show_menu() { + local menu_item + local -a menu_items=( "apply" "revert" "help" "quit" ) + local PS3="($PROGNAME)> " # set custom prompt + + cat >&2 <<- EOF + Welcome to $PROGNAME ($VERSION)! + Type 'help' to view a list of commands or enter + the number of the action you want to execute. + + EOF + + select menu_item in "${menu_items[@]}"; do + case "${menu_item:-$REPLY}" in + apply|[aA]*) apply ;; + revert|[rR]*) revert ;; + help|[hH]*) + cat >&2 <<- EOF + apply - fixes hardcoded icons of installed applications + revert - reverts any changes made + help - displays this help menu + clear - clear the screen + quit - quit $PROGNAME + EOF + ;; + clear|[cC]*) clear ;; + quit|[qQeE]*) exit 0 ;; + *) err "invalid command -- '$REPLY'" + esac + done < /dev/tty # don't read from stdin +} + +main() { + if [ "$(id -u)" -eq 0 ]; then + fatal "This script must be run as normal user." + fi + + if [ "${#ARGS[@]}" -gt 0 ]; then + parse_opts "${ARGS[@]}" + else + show_menu + fi +} + +main + +exit 0