#!/usr/bin/env bash # Author: Lu Xu # License: MIT # References: https://specifications.freedesktop.org/desktop-entry-spec # https://specifications.freedesktop.org/icon-theme-spec # XDG directories XDG_DATA_HOME=${XDG_DATA_HOME:-$HOME/.local/share} XDG_CACHE_HOME=${XDG_CACHE_HOME:-$HOME/.cache} XDG_CONFIG_HOME=${XDG_CONFIG_HOME:-$HOME/.config} XDG_DATA_DIRS=${XDG_DATA_DIRS:-/usr/share:/usr/local/share} # Apply default configuration values SHOW_ALL=${XDG_MENU_SHOW_ALL:-} TERMINAL=${XDG_MENU_TERMINAL:-xterm} SIZE=${XDG_MENU_ICON_SIZE:-24} ICON_THEME=${XDG_MENU_ICON_THEME:-$( if command -v gtk-query-settings > /dev/null; then gtk-query-settings gtk-icon-theme-name | awk '{ print $2 }' | tr -d '"' else echo Adwaita fi )} FALLBACK_ICON=${XDG_MENU_FALLBACK_ICON:-application-x-executable} # Read CLI arguments while getopts "ab:fhi:ns:t:" arg; do case "$arg" in a) SHOW_ALL=1;; b) FALLBACK_ICON=$OPTARG;; f) force_cache=1;; i) ICON_THEME=$OPTARG;; n) not_cache=1;; s) SIZE=$OPTARG;; t) TERMINAL=$OPTARG;; h|*) echo "$0 [-a] [-b icon] [-f] [-i theme] [-n] [-s size] [-t term]" && exit ;; esac done # Directories DATA_DIRS=$XDG_DATA_HOME:$XDG_DATA_DIRS CACHE_DIR="$XDG_CACHE_HOME/xdg-xmenu" CACHE_ICON_DIR="$CACHE_DIR/icons" mkdir -p "$CACHE_ICON_DIR" # Find directories in an icon theme matching the target size valid_dirs() { theme_dir=$1 [ -f "$theme_dir/index.theme" ] || return while IFS='=' read -r key value; do # parse the index.theme file case "$key" in \[*\]) # FIXME: scaled icons are ignored for now, sorry HiDPI # deal with last subdirectory, only Size is mandatory if [ "${Scale-1}" = 1 ] && [ -n "$Size" ]; then # defaults if they are not specified Type=${Type:-Threshold} MaxSize=${MaxSize:-Size} MinSize=${MinSize:-Size} Threshold=${Threshold:-2} # match subdirectory sizes based on 'Type' # 1. fixed size, has to be exact { [ "$Type" = Fixed ] && [ "$SIZE" = "$Size" ]; } || # 2. threshold size, within Threshold times { [ "$Type" = Threshold ] && [ "$SIZE" -ge "$((Size / Threshold))" ] && [ "$SIZE" -le "$((Size * Threshold))" ]; } || # 3. scalable size, Min/MaxSize as lower/upper limits { [ "$Type" = Scalable ] && [ "$SIZE" -ge "$MinSize" ] && [ "$SIZE" -le "$MaxSize" ]; } && # found it echo "$theme_dir/$subdir" fi # clean up variables for the new subdirectory unset MaxSize MinSize Scale Size Threshold Type subdir subdir=${key#[} subdir=${subdir%]} ;; *) # read each key-value pair into shell variables [ -n "$value" ] && eval "$key=\"$value\"" ;; esac done < "$theme_dir/index.theme" } # Find all directories matching the size and containing icons directly find_all_dirs() { IFS=':' for datadir in $DATA_DIRS; do unset IFS for theme in "$ICON_THEME" "hicolor"; do valid_dirs "$datadir/icons/$theme" done done echo "/usr/share/pixmaps" } all_dirs=$(find_all_dirs) # Search for an icon based on its name find_icon() { icon=$1 # find any png, svg or xpm icon in available directories for dir in $all_dirs; do for ext in png svg xpm; do if [ -f "$dir/$icon.$ext" ]; then echo "$dir/$icon.$ext" return fi done done # when the fallback icon is not found, return none instead [ "$icon" = "$FALLBACK_ICON" ] && echo "" || echo "$FALLBACK_ICON" } # xmenu does not support svg, so convert svg to png cache_icon() { icon=$1 # icon is a existing png/xpm file [ -f "$icon" ] && [ ${icon##*.} != 'svg' ] && echo "$icon" && return # cache path in $CACHE_ICON_DIR with the flattened name [ -f "$icon" ] && basename=$(echo $icon | tr '/' '_') || basename=${icon##*/} cache_path=$CACHE_ICON_DIR/$basename.png # already cached, skip unless forced to cache again if [ -f "$cache_path" ] && [ -z "$force_cache" ]; then echo "$cache_path" && return fi # find the icon in icon themes if the name is not a path [ -f "$icon" ] && icon_path=$icon || icon_path=$(find_icon "$icon") # convert svg -> png if [ "${icon_path##*.}" = "svg" ]; then # first try the fasted, then the slower ones if [ -n "$not_cache" ]; then : # not doing caching in dry run mode elif command -v rsvg-convert > /dev/null; then rsvg-convert -w "$SIZE" -h "$SIZE" "$icon_path" -o "$cache_path" elif command -v convert > /dev/null; then convert -background none -size "${SIZE}x${SIZE}" "$icon_path" "$cache_path" elif command -v inkscape > /dev/null; then inkscape -w "$SIZE" -h "$SIZE" "$icon_path" -o "$cache_path" else echo >&2 "[Error]: Convert svg icons failed." echo >&2 "Please install one of the following programs:" echo >&2 "rsvg-convert(librsvg), convert(imagemagick), inkscape" exit 1 fi echo "$cache_path" else # use png and xpm icons in-place # this could be empty if no fallback icon is found, and it will work echo "$icon_path" fi } FALLBACK_ICON=$(cache_icon "$FALLBACK_ICON") # parse the desktop entries and categorize them IFS=: for datadir in $DATA_DIRS; do unset IFS appdir="$datadir/applications" [ ! -d "$appdir" ] && continue for app in "$appdir"/*.desktop; do unset icon name genname cmd category nodisplay useterm added while IFS="=" read -r key value; do case "$key" in Categories) [ -z "$category" ] && category=$value ;; Icon) [ -z "$icon" ] && icon=$(cache_icon "$value") ;; Name) [ -z "$name" ] && name="$value" ;; GenericName)[ -z "$genname" ] && genname=$value ;; Exec) [ -z "$cmd" ] && cmd=$value && # remove %* place holders [ "${cmd#*%}" != "$cmd" ] && cmd=$( echo "$cmd" | sed 's/ %[fFuUick]//g' | sed 's/%%/$/g' ) ;; NoDisplay) [ "$value" != "false" ] && nodisplay=1 ;; Terminal) [ "$value" != "false" ] && useterm=1 ;; # FIXME: show_all not working as it should OnlyShowIn) [ -z "$SHOW_ALL" ] && nodisplay=1 ;; esac done < "$app" [ -n "$nodisplay" ] && continue [ -n "$useterm" ] && cmd="$TERMINAL -e $cmd" [ -n "$genname" ] && name="$name ($genname)" icon="${icon-$FALLBACK_ICON}" icon="${icon:+\tIMG:$icon}" item="$icon\t$name\t$cmd" IFS=\; for c in $category; do unset IFS case "$c" in Audio*|*Video) added=1; Multimedia="$Multimedia$item\n" ;; Development) added=1; Development="$Development$item\n" ;; Education) added=1; Education="$Education$item\n" ;; Game) added=1; Games="$Games$item\n" ;; Graphics) added=1; Graphics="$Graphics$item\n" ;; Network) added=1; Internet="$Internet$item\n" ;; Office) added=1; Office="$Office$item\n" ;; Science) added=1; Science="$Science$item\n" ;; Settings) added=1; Settings="$Settings$item\n" ;; System) added=1; System="$System$item\n" ;; Utility) added=1; Accessories="$Accessories$item\n" ;; esac done [ -z "$added" ] && Others="$Others$item\n" done done category_sort() { category_icon=$1 category_name=$2 category_apps=$3 icon=$(cache_icon "$category_icon") # create top level folder printf "${icon:+IMG:$icon\t}%s\n" "$category_name" # sort the applications by their names # FIXME: when icon is missing, name is in first column printf "$category_apps%s" | sort -f -k 2 | uniq } category_sort applications-accessories Accessories "$Accessories" category_sort applications-development Development "$Development" category_sort applications-education Education "$Education" category_sort applications-games Games "$Games" category_sort applications-graphics Graphics "$Graphics" category_sort applications-internet Internet "$Internet" category_sort applications-multimedia Multimedia "$Multimedia" category_sort applications-office Office "$Office" category_sort applications-science Science "$Science" category_sort preferences-desktop Settings "$Settings" category_sort applications-system System "$System" category_sort applications-other Others "$Others"