123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428 |
- #!/bin/bash
- _usage() {
- if [ -t 0 ]; then [[ "$*" != "" ]] && echo "$*
- "
- cat <<EOF
- Openbox pipemenu to create a list of recent files with or without icons.
- Usage: $0 [options]
- Options:
- -m int maximum number of results to display. Default: $max
- -s str Consider icon sizes between min-max. Default: $isizes
- -n No icons at all. Default is to use icons.
- -f No full path on menu label, only filename
- -x str Application to mime-open files. xdg-open has some pitfalls when no DE
- is defined, mimeo (xyne.archlinux.ca/projects/mimeo/) might be more
- robust. Default: $mimeopener
- -r str Recentfile to parse. Default: ${recentfile/$HOME/\~}
- -i str Full path to mimetype icon directory to store symlinks to exisiting
- icons. Default: $icondir
- -g str GTK rc/ini file to extract icon theme from.
- Default: ${gtksettingsfile/$HOME/\~}
- -c Clear the recentfile (access from pipemenu).
- -d Output debug info to stderr
- -O str str Attempt to find an icon for the supplied mimetype and category,
- then exit.
- Dependencies:
- xmlstarlet: parsing recentfile
- Optional:
- gio (glib2, libglib2.0-bin): get icons
- EOF
- else
- echo "<openbox_pipe_menu>"
- [[ "$*" != "" ]] && echo "<item label=\"$*\"/>"
- _endmenu
- fi
- exit 1
- }
- _debug() { :; } # redefine this func when debug is enabled
- _G() {
- RETVAL=""
- # simplistic grep replacement
- # only works on files, only the first match is assigned to a variable
- # uses regex (what kind of regex?!)
- # $1: regex
- # $2: file
- while read -r line; do
- [[ "$line" =~ $1 ]] && {
- RETVAL="$line"; _debug ">>--- G: found $1 in $2"; return; }
- done <"$2"
- return 1
- }
- _getdirs() {
- # get all relevant (see isizes) icon directories from one theme and append
- # to the icondirlist array.
- # $1: icontheme's index.theme
- _debug "####################### _getdirs: Getting and filtering Directories for $1 #######################"
- _G "^Directories=" "$1"
- local dirs="${RETVAL#*=}" # remove leading Directories=
- dirs="${dirs%,}" # Remove a possible trailing comma
- oldifs="$IFS"; IFS=, # temporarily change IFS to comma
- dirs=($dirs)
- IFS="$oldifs"
- _debug "## All the subdirectories of ${1%*/}: ${dirs[*]} #######################"
- # Filer out what we want: $mimequery, $isizes
- smallest="${isizes%-*}"
- largest="${isizes#*-}"
- basedir="${1%/*}" # always prepend base directory to icon dir
- for query in "${mimequery[@]}"; do
- for dir in "${dirs[@]}"; do
- _debug "### $basedir/$dir "
- if [[ "$dir" == *"$query"* ]]; then
- # isolate subdir:
- size="${dir//$query/}"; size="${size//\//}"
- [[ "$size" == *'@'* ]] && continue # we don't want these at all
- if [[ "$size" == scalable ]]; then
- _debug ">=>=>=>=>=> Found scalable in $dir, adding it to icondirlist."
- icondirlist[$((I++))]="$basedir/$dir"
- else
- # isolate only 1st number by removing all letters that follow:
- size="${size%%[a-zA-Z-_]*}"
- _debug "Size is $size - largest is $largest, smallest is $smallest"
- # Is this number between smallest and largest? Then add it to icondirlist
- (( size <= largest && size >= smallest )) && { _debug ">=>=>=>=>=> Found $size in $dir, adding it to icondirlist."; icondirlist[$((I++))]="$basedir/$dir"; }
- fi
- fi
- done
- done
- }
- _loaddirlist() {
- _debug "_useicons: Compile icon search paths"
- if [ -r "$icondirlist_file" ]; then
- _debug "Found $icondirlist_file - no need to compile it."
- mapfile -t icondirlist <"$icondirlist_file"
- else
- # specifications.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html
- # Each theme is stored as subdirectories of the base directories.
- # The internal name of the theme is the name of the subdirectory, although the user-visible name as specified by the theme may be different.
- # Hence, theme names are case sensitive, and are limited to ASCII characters. Theme names may also not contain comma or space.
- mkdir -p "$icondir" || _usage "Option -${opt}: Cannot create directory $icondir"
- iconthemepath=""
- iconthemeinherits=""
- allinherits=()
- themepath=""
- a=0
- _themepath() {
- for path in "${XDG_DATA_HOME-"$HOME/.local/share"}/icons" "/usr/share/icons"; do
- path="$path/$1/index.theme"
- [ -r "$path" ] && { _debug "_themepath: found $path"; RETVAL="$path"; return 0; }
- done
- RETVAL=""; _debug "_themepath: no path found for $1"; return 1
- }
- _inherits() {
- # this function is calling itself until it finds no more Inherits
- # $1: themepath index.theme
- _G '^Inherits=' "$1" || { _debug "No Inherits found"; return 1; }
- iconthemeinherits="${RETVAL#*=}"
- oldifs="$IFS"; IFS=, # temporarily change IFS to comma
- iconthemeinherits=($iconthemeinherits)
- IFS="$oldifs"
- ### now extract dirlist for each inherit and continue filling icondirlist array and icondirlist_file
- for theme in "${iconthemeinherits[@]}"; do
- for inh in "${allinherits[@]}"; do
- [[ "$inh" == "$theme" ]] && continue 2
- allinherits[a++]="$theme"
- done
- _debug "Inherit: $theme"
- if _themepath "$theme"; then
- themepath="$RETVAL"
- _getdirs "$themepath"; _inherits "$themepath"
- fi
- done
- return 0
- }
- _debug "Getting current icon theme & inherits."
- _G gtk-icon-theme-name "$gtksettingsfile" || { useicons=0; _debug "no icon theme found"; return 1; }
- icontheme="${RETVAL##*=}" # the internal name of the theme
- icontheme="${icontheme#*\"}"
- icontheme="${icontheme%\"*}"
- allinherits[a++]="$icontheme"
- _themepath "$icontheme" || { _debug "no path found for icontheme \"$icontheme\""; return 1; }
- themepath="$RETVAL"
- _getdirs "$themepath"; _inherits "$themepath"
- # icondirlist is now compiled; write it out to icondirlist_file
- printf "%s\n" "${icondirlist[@]}" > "$icondirlist_file"
- fi
- }
- _useicons() {
- _loaddirlist || return 1
- ###########################################################################
- _debug "==== Collecting icon choices for ${#array_file[@]} files ===="
- # to get only the last line of the gio output for each file we use mapfile with
- # a callback function, see: wiki.bash-hackers.org/commands/builtin/mapfile#the_callback
- mtf() {
- # $1: index of what mapfile is processing right now
- # $2: string contained in that index
- local string="${2##*:}" # remove "standard::icon" from front
- string="${string// /}" # remove spaces => nice comma-separated list of icons
- array_icon[$(($1/5))]="$string"
- _debug "mapfile index $1: ${array_file[$(($1/5))]}: $string"
- }
- mapfile -t -c 5 -C 'mtf ' <<<"$(gio info -a standard::icon "${array_file[@]#*$sep}")"
- }
- _geticon() {
- # gets the icon from a dir from the icondirlist
- # $1: comma-separated list of icon names
- # #not used anymore $2: category of icon: [mime|apps|actions|places]
- RETVAL=""
- for list in "$1" "$unknownicon"; do
- _debug "_geticon: searching for $list in $2 dirs"
- oldifs="$IFS"; IFS=, # temporarily change IFS to comma
- names=($list) # convert to array
- IFS="$oldifs"
- _debug "names: ${names[*]}"
- # first, let's see if we have it already
- for name in "${names[@]}"; do
- for found in "$icondir/$name".???; do
- [ -r "$found" ] && { RETVAL="$found"; _debug "Found it already in icondirlist: $found"; return; }
- done
- done
- # search each dir in icondirlist
- for name in "${names[@]}"; do
- for dir in "${icondirlist[@]}"; do
- #~ # skip if it's not the desired category (mime/actions/places/apps)
- #~ [[ "$dir" != *"$2"* ]] && continue
- for found in "$dir/$name".???; do
- _debug "Searching for $found ..."
- if [ -r "$found" ]; then
- _debug "Found it! And return."
- ext="${found##*.}"
- # found! make a symlink...
- ln -s "$found" "$icondir/$name.$ext" >&2
- # ... and return that
- RETVAL="$icondir/$name.$ext"
- return 0
- fi
- done
- done
- done
- done
- return 1
- }
- _endmenu() {
- if ((allentries>0)); then
- echo -n "<separator/><item"
- [[ "$useicons" == 1 ]] && _geticon "$deleteicon" && echo -n " icon=\"$RETVAL\""
- echo -n " label=\"Clear recents… ($allentries entries"
- ((unreadable>0)) && echo -n " - $unreadable unreadable"
- echo ")\">
- <action name=\"Execute\"><prompt>Really delete ${recentfile//$HOME/\~}?</prompt><command><![CDATA[\"$0\" -c]]></command></action>
- </item>"
- fi
- if [ -d "$icondir" ]; then
- echo -n "<item label=\"Clear icon cache…\" "
- [[ "$useicons" == 1 ]] && _geticon "$deleteicon" && echo -n " icon=\"$RETVAL\""
- echo ">
- <action name=\"Execute\"><prompt>Really delete ${icondir//$HOME/\~}?</prompt><command><![CDATA[rm -r \"$icondir\"]]></command></action>
- </item>"
- fi
- echo "</openbox_pipe_menu>"
- }
- _decode() {
- # unescape from XML
- RETVAL="${1//&/&}"
- RETVAL="${RETVAL//'/\'}"
- RETVAL="${RETVAL//"/\"}"
- # decode url encoding - unix.stackexchange.com/a/187256
- LC_ALL=C printf -v RETVAL "%b" "${RETVAL//%/\\x}"
- }
- #\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
- #||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
- #//////////////////////////////////////////////////////////////////////////////
- max=20 # maximum number of recent files
- recentfile="${XDG_DATA_HOME-"$HOME/.local/share"}/recently-used.xbel"
- useicons=1
- fullpath=1
- mimeopener="xdg-open"
- dep=( xmlstarlet )
- sep='|'
- only_get_icon=0
- gtksettingsfile="${XDG_CONFIG_HOME-"$HOME/.config"}/gtk-3.0/settings.ini"
- icondir="${XDG_CONFIG_HOME-"$HOME/.config"}/openbox/mimetype_icons"
- isizes=22-64 # exactly two minus-separated icon sizes: smallest-largest
- execute_and_update=0 # execute and update
- allentries=0 # counting all entries in recentfile...
- unreadable=0 # ... of which some are unreadable
- # initialise a global RETVAL to be used in functions - avoids opening a subshell
- # https://rus.har.mn/blog/2010-07-05/subshells/
- RETVAL=''
- while getopts "dcs:m:r:g:i:nfx:XOh" opt; do
- case $opt in
- d) _debug() {
- echo "[31m${*}(B[m" >&2
- return 0
- }
- ;;
- c)
- cat <<EOF > "$recentfile"
- <?xml version="1.0" encoding="UTF-8"?>
- <xbel version="1.0"
- xmlns:bookmark="http://www.freedesktop.org/standards/desktop-bookmarks"
- xmlns:mime="http://www.freedesktop.org/standards/shared-mime-info"
- >
- </xbel>
- EOF
- exit
- ;;
- s) isizes="$OPTARG"
- ;;
- m) [[ "$OPTARG" =~ [0-9]+ ]] && (( OPTARG >= 0 )) && (( OPTARG <= 65535 )) || _usage "Option -${opt}: invalid number $OPTARG"
- max="$OPTARG"
- ;;
- r) [ -r "$OPTARG" ] || _usage "Option -${opt}: Cannot read $OPTARG"
- recentfile="$OPTARG"
- ;;
- g) [ -r "$OPTARG" ] || _usage "Option -${opt}: Cannot read $OPTARG"
- gtksettingsfile="$OPTARG"
- ;;
- i) [[ "$OPTARG" == /* ]] || _usage "Option -${opt}: Invalid path $OPTARG - must be absolute"
- icondir="$OPTARG"
- ;;
- n) useicons=0
- ;;
- f) fullpath=0
- ;;
- x) if [ -n "$OPTARG" ]; then
- type "$OPTARG" >/dev/null 2>&1 || _usage "Option -${opt}: $OPTARG is not in PATH"
- mimeopener="$OPTARG"
- else
- mimeopener=""
- fi
- ;;
- X) # execute command & update recentfile
- execute_and_update=1
- ;;
- O) # only get an icon
- only_get_icon=1
- ;;
- h) _usage
- ;;
- *) _usage "Invalid option -$OPTARG"
- ;;
- esac
- done
- shift $((OPTIND-1))
- # called by the pipemenu itself to execute commands and simultaneously update $recentfile
- if [[ "$execute_and_update" == 1 ]]; then
- [ -t 0 ] && echo "This option should only be called by the pipemenu itself." && exit 1
- item="$1"; shift
- recentfile="$1"; shift
- "$@" & # executing command
- # Now updating recently-used file
- # date format: xbel.sourceforge.net/language/versions/1.0/xbel-1.0.xhtml and www.w3.org/TR/NOTE-datetime
- TZ=UTC printf -v time "%(%FT%R:%S.499999Z)T" # bash doesn't deal with fractions of seconds, they have been replaced with 499999
- tmp="$(xmlstarlet ed -u "/xbel/bookmark[@href=\"$item\"]/@visited" -v "$time" "$recentfile")"
- [[ "$tmp" != "" ]] && echo "$tmp" > "$recentfile"
- exit
- fi
- # a few things that are required only when you use icons:
- if [[ "$useicons" == 1 ]]; then
- icondirlist_file="$icondir/icondirlist"
- icondirlist=() # global list of icn directories to search for mimetypes
- I=0 # global counter for icondirlist
- mimequery=( mimes mimetypes apps places )
- deleteicon="user-trash,user-trash-symbolic"
- unknownicon="unknown,stock_unknown,gnome-unknown"
- [[ "$only_get_icon" == 1 ]] && { _loaddirlist; echo "$1"; echo "$2"; _geticon "$1"; echo "$RETVAL"; exit; }
- fi
- # quick dependency check
- type xmlstarlet >/dev/null || usage "Dependency missing: $x"
- # Use an Xpath expression to collect all
- # 1) file:///...
- # 2) application
- # elements, sorted by last modified date. Read into array and limit to $max elements.
- mapfile -t array_raw <<<"$(xmlstarlet sel -t -m "/xbel/bookmark" -s D:T:U '@visited' -n -v '@href' -o "$sep" -m 'info/metadata/bookmark:applications/bookmark:application' -s D:T:U '@modified' -v '@exec' -o "$sep" "$recentfile")"
- [[ "${array_raw[*]}" == "" ]] && _usage "No recent files."
- allentries="$(( ${#array_raw[@]} - 1))"
- [ -z "$mimeopener" ] || type "$mimeopener" >/dev/null 2>&1 || mimeopener="" >&2
- array_file=()
- array_cmd=()
- # start counting at 1 because the first array element is always an empty line due to the xmlstarlet command
- for ((i=1,j=0;i<${#array_raw[@]};i++)); do
- (( j >= max )) && break
- _debug "Raw line: ${array_raw[i]}"
- oldifs="$IFS"; IFS="$sep"
- line=(${array_raw[i]})
- IFS="$oldifs"
- # filepath - remove file:// and quotes, then decode both XML escapes and URL encoding
- _decode "${line[0]#*file://}"
- file="$RETVAL"
- # drop unreadable files here
- [ -r "$file" ] || { _debug "Unreadable: $file"; ((unreadable++)); continue; }
- ##########################
- _debug "Readable: ${line[0]}${sep}$file"
- array_file[j]="${line[0]}${sep}$file"
- for ((k=1;k<${#line[@]};k++)); do
- cmd="${line[k]}"
- # command to open file
- # remove single quotes, spaces and %u
- cmd="${cmd#*\'}"
- cmd="${cmd% %u\'}"
- type "${cmd%% *}" >/dev/null 2>&1 && array_cmd[j]="$cmd,${array_cmd[j]}"
- done
- # append $mimeopener, if defined, otherwise remove trailing comma
- [ -n "$mimeopener" ] && array_cmd[j]="${array_cmd[j]}$mimeopener" || array_cmd[j]="${array_cmd[j]%,}"
- _debug "Command: ${array_cmd[j]}"
- ((j++))
- done
- unset array_raw
- # without icons, that is all. Otherwise:
- if [[ "$useicons" == 1 ]]; then
- type gio >/dev/null || usage "Dependency missing for icons: gio"
- array_icon=()
- _useicons
- fi
- # make the pipemenu
- echo "<openbox_pipe_menu>"
- for ((i=0;i<${#array_file[@]};i++)); do
- # label of the menu entry - openbox-specific
- label="${array_file[i]#*$sep}"
- _debug "whole line: ${array_file[i]} - label: $label"
- [[ "$fullpath" == 1 ]] && label="${label/$HOME/\~}" || label="${label##*/}"
- # encode for XML
- # ampersands must be changed first:
- label="${label//&/&}"
- label="${label//\"/"}"
- label="${label//</<}"
- label="${label//>/>}"
- label="${label//_/__}"
- _debug "Label: ${label}"
- echo -n "<menu id=\"$label $i\" label=\"$label\""
- # add icon if applicable
- if [[ "$useicons" == 1 ]]; then
- #~ [ -d "${array_file[i]#*$sep}" ] &&
- _geticon "${array_icon[i]}"
- echo -n " icon=\"$RETVAL\""
- fi
- echo ">" # finalise the opening menu tag
- # command(s) to execute on the file
- oldifs="$IFS"; IFS=","
- cmd=(${array_cmd[i]})
- IFS="$oldifs"
- for c in "${cmd[@]}"; do
- exelabel="Open with $c"
- # escaping single quotes in the file path: stackoverflow.com/a/1315213
- exe="${array_file[i]#*${sep}}"
- exe="${exe//\'/\'\\\'\'}"
- #~ exe="'$c' '$exe'"
- _debug "Execute: $c $exe"
- echo "<item label=\"$exelabel\"><action name=\"Execute\"><command><![CDATA['$0' '-X' \"${array_file[i]%%${sep}*}\" \"$recentfile\" $c '$exe']]></command></action></item>"
- done
- echo "</menu>"
- done
- _endmenu
|