recent 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  1. #!/bin/bash
  2. _usage() {
  3. if [ -t 0 ]; then [[ "$*" != "" ]] && echo "$*
  4. "
  5. cat <<EOF
  6. Openbox pipemenu to create a list of recent files with or without icons.
  7. Usage: $0 [options]
  8. Options:
  9. -m int maximum number of results to display. Default: $max
  10. -s str Consider icon sizes between min-max. Default: $isizes
  11. -n No icons at all. Default is to use icons.
  12. -f No full path on menu label, only filename
  13. -x str Application to mime-open files. xdg-open has some pitfalls when no DE
  14. is defined, mimeo (xyne.archlinux.ca/projects/mimeo/) might be more
  15. robust. Default: $mimeopener
  16. -r str Recentfile to parse. Default: ${recentfile/$HOME/\~}
  17. -i str Full path to mimetype icon directory to store symlinks to exisiting
  18. icons. Default: $icondir
  19. -g str GTK rc/ini file to extract icon theme from.
  20. Default: ${gtksettingsfile/$HOME/\~}
  21. -c Clear the recentfile (access from pipemenu).
  22. -d Output debug info to stderr
  23. -O str str Attempt to find an icon for the supplied mimetype and category,
  24. then exit.
  25. Dependencies:
  26. xmlstarlet: parsing recentfile
  27. Optional:
  28. gio (glib2, libglib2.0-bin): get icons
  29. EOF
  30. else
  31. echo "<openbox_pipe_menu>"
  32. [[ "$*" != "" ]] && echo "<item label=\"$*\"/>"
  33. _endmenu
  34. fi
  35. exit 1
  36. }
  37. _debug() { :; } # redefine this func when debug is enabled
  38. _G() {
  39. RETVAL=""
  40. # simplistic grep replacement
  41. # only works on files, only the first match is assigned to a variable
  42. # uses regex (what kind of regex?!)
  43. # $1: regex
  44. # $2: file
  45. while read -r line; do
  46. [[ "$line" =~ $1 ]] && {
  47. RETVAL="$line"; _debug ">>--- G: found $1 in $2"; return; }
  48. done <"$2"
  49. return 1
  50. }
  51. _getdirs() {
  52. # get all relevant (see isizes) icon directories from one theme and append
  53. # to the icondirlist array.
  54. # $1: icontheme's index.theme
  55. _debug "####################### _getdirs: Getting and filtering Directories for $1 #######################"
  56. _G "^Directories=" "$1"
  57. local dirs="${RETVAL#*=}" # remove leading Directories=
  58. dirs="${dirs%,}" # Remove a possible trailing comma
  59. oldifs="$IFS"; IFS=, # temporarily change IFS to comma
  60. dirs=($dirs)
  61. IFS="$oldifs"
  62. _debug "## All the subdirectories of ${1%*/}: ${dirs[*]} #######################"
  63. # Filer out what we want: $mimequery, $isizes
  64. smallest="${isizes%-*}"
  65. largest="${isizes#*-}"
  66. basedir="${1%/*}" # always prepend base directory to icon dir
  67. for query in "${mimequery[@]}"; do
  68. for dir in "${dirs[@]}"; do
  69. _debug "### $basedir/$dir "
  70. if [[ "$dir" == *"$query"* ]]; then
  71. # isolate subdir:
  72. size="${dir//$query/}"; size="${size//\//}"
  73. [[ "$size" == *'@'* ]] && continue # we don't want these at all
  74. if [[ "$size" == scalable ]]; then
  75. _debug ">=>=>=>=>=> Found scalable in $dir, adding it to icondirlist."
  76. icondirlist[$((I++))]="$basedir/$dir"
  77. else
  78. # isolate only 1st number by removing all letters that follow:
  79. size="${size%%[a-zA-Z-_]*}"
  80. _debug "Size is $size - largest is $largest, smallest is $smallest"
  81. # Is this number between smallest and largest? Then add it to icondirlist
  82. (( size <= largest && size >= smallest )) && { _debug ">=>=>=>=>=> Found $size in $dir, adding it to icondirlist."; icondirlist[$((I++))]="$basedir/$dir"; }
  83. fi
  84. fi
  85. done
  86. done
  87. }
  88. _loaddirlist() {
  89. _debug "_useicons: Compile icon search paths"
  90. if [ -r "$icondirlist_file" ]; then
  91. _debug "Found $icondirlist_file - no need to compile it."
  92. mapfile -t icondirlist <"$icondirlist_file"
  93. else
  94. # specifications.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html
  95. # Each theme is stored as subdirectories of the base directories.
  96. # 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.
  97. # Hence, theme names are case sensitive, and are limited to ASCII characters. Theme names may also not contain comma or space.
  98. mkdir -p "$icondir" || _usage "Option -${opt}: Cannot create directory $icondir"
  99. iconthemepath=""
  100. iconthemeinherits=""
  101. allinherits=()
  102. themepath=""
  103. a=0
  104. _themepath() {
  105. for path in "${XDG_DATA_HOME-"$HOME/.local/share"}/icons" "/usr/share/icons"; do
  106. path="$path/$1/index.theme"
  107. [ -r "$path" ] && { _debug "_themepath: found $path"; RETVAL="$path"; return 0; }
  108. done
  109. RETVAL=""; _debug "_themepath: no path found for $1"; return 1
  110. }
  111. _inherits() {
  112. # this function is calling itself until it finds no more Inherits
  113. # $1: themepath index.theme
  114. _G '^Inherits=' "$1" || { _debug "No Inherits found"; return 1; }
  115. iconthemeinherits="${RETVAL#*=}"
  116. oldifs="$IFS"; IFS=, # temporarily change IFS to comma
  117. iconthemeinherits=($iconthemeinherits)
  118. IFS="$oldifs"
  119. ### now extract dirlist for each inherit and continue filling icondirlist array and icondirlist_file
  120. for theme in "${iconthemeinherits[@]}"; do
  121. for inh in "${allinherits[@]}"; do
  122. [[ "$inh" == "$theme" ]] && continue 2
  123. allinherits[a++]="$theme"
  124. done
  125. _debug "Inherit: $theme"
  126. if _themepath "$theme"; then
  127. themepath="$RETVAL"
  128. _getdirs "$themepath"; _inherits "$themepath"
  129. fi
  130. done
  131. return 0
  132. }
  133. _debug "Getting current icon theme & inherits."
  134. _G gtk-icon-theme-name "$gtksettingsfile" || { useicons=0; _debug "no icon theme found"; return 1; }
  135. icontheme="${RETVAL##*=}" # the internal name of the theme
  136. icontheme="${icontheme#*\"}"
  137. icontheme="${icontheme%\"*}"
  138. allinherits[a++]="$icontheme"
  139. _themepath "$icontheme" || { _debug "no path found for icontheme \"$icontheme\""; return 1; }
  140. themepath="$RETVAL"
  141. _getdirs "$themepath"; _inherits "$themepath"
  142. # icondirlist is now compiled; write it out to icondirlist_file
  143. printf "%s\n" "${icondirlist[@]}" > "$icondirlist_file"
  144. fi
  145. }
  146. _useicons() {
  147. _loaddirlist || return 1
  148. ###########################################################################
  149. _debug "==== Collecting icon choices for ${#array_file[@]} files ===="
  150. # to get only the last line of the gio output for each file we use mapfile with
  151. # a callback function, see: wiki.bash-hackers.org/commands/builtin/mapfile#the_callback
  152. mtf() {
  153. # $1: index of what mapfile is processing right now
  154. # $2: string contained in that index
  155. local string="${2##*:}" # remove "standard::icon" from front
  156. string="${string// /}" # remove spaces => nice comma-separated list of icons
  157. array_icon[$(($1/5))]="$string"
  158. _debug "mapfile index $1: ${array_file[$(($1/5))]}: $string"
  159. }
  160. mapfile -t -c 5 -C 'mtf ' <<<"$(gio info -a standard::icon "${array_file[@]#*$sep}")"
  161. }
  162. _geticon() {
  163. # gets the icon from a dir from the icondirlist
  164. # $1: comma-separated list of icon names
  165. # #not used anymore $2: category of icon: [mime|apps|actions|places]
  166. RETVAL=""
  167. for list in "$1" "$unknownicon"; do
  168. _debug "_geticon: searching for $list in $2 dirs"
  169. oldifs="$IFS"; IFS=, # temporarily change IFS to comma
  170. names=($list) # convert to array
  171. IFS="$oldifs"
  172. _debug "names: ${names[*]}"
  173. # first, let's see if we have it already
  174. for name in "${names[@]}"; do
  175. for found in "$icondir/$name".???; do
  176. [ -r "$found" ] && { RETVAL="$found"; _debug "Found it already in icondirlist: $found"; return; }
  177. done
  178. done
  179. # search each dir in icondirlist
  180. for name in "${names[@]}"; do
  181. for dir in "${icondirlist[@]}"; do
  182. #~ # skip if it's not the desired category (mime/actions/places/apps)
  183. #~ [[ "$dir" != *"$2"* ]] && continue
  184. for found in "$dir/$name".???; do
  185. _debug "Searching for $found ..."
  186. if [ -r "$found" ]; then
  187. _debug "Found it! And return."
  188. ext="${found##*.}"
  189. # found! make a symlink...
  190. ln -s "$found" "$icondir/$name.$ext" >&2
  191. # ... and return that
  192. RETVAL="$icondir/$name.$ext"
  193. return 0
  194. fi
  195. done
  196. done
  197. done
  198. done
  199. return 1
  200. }
  201. _endmenu() {
  202. if ((allentries>0)); then
  203. echo -n "<separator/><item"
  204. [[ "$useicons" == 1 ]] && _geticon "$deleteicon" && echo -n " icon=\"$RETVAL\""
  205. echo -n " label=\"Clear recents… ($allentries entries"
  206. ((unreadable>0)) && echo -n " - $unreadable unreadable"
  207. echo ")\">
  208. <action name=\"Execute\"><prompt>Really delete ${recentfile//$HOME/\~}?</prompt><command><![CDATA[\"$0\" -c]]></command></action>
  209. </item>"
  210. fi
  211. if [ -d "$icondir" ]; then
  212. echo -n "<item label=\"Clear icon cache…\" "
  213. [[ "$useicons" == 1 ]] && _geticon "$deleteicon" && echo -n " icon=\"$RETVAL\""
  214. echo ">
  215. <action name=\"Execute\"><prompt>Really delete ${icondir//$HOME/\~}?</prompt><command><![CDATA[rm -r \"$icondir\"]]></command></action>
  216. </item>"
  217. fi
  218. echo "</openbox_pipe_menu>"
  219. }
  220. _decode() {
  221. # unescape from XML
  222. RETVAL="${1//&amp;/&}"
  223. RETVAL="${RETVAL//&apos;/\'}"
  224. RETVAL="${RETVAL//&quot;/\"}"
  225. # decode url encoding - unix.stackexchange.com/a/187256
  226. LC_ALL=C printf -v RETVAL "%b" "${RETVAL//%/\\x}"
  227. }
  228. #\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
  229. #||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
  230. #//////////////////////////////////////////////////////////////////////////////
  231. max=20 # maximum number of recent files
  232. recentfile="${XDG_DATA_HOME-"$HOME/.local/share"}/recently-used.xbel"
  233. useicons=1
  234. fullpath=1
  235. mimeopener="xdg-open"
  236. dep=( xmlstarlet )
  237. sep='|'
  238. only_get_icon=0
  239. gtksettingsfile="${XDG_CONFIG_HOME-"$HOME/.config"}/gtk-3.0/settings.ini"
  240. icondir="${XDG_CONFIG_HOME-"$HOME/.config"}/openbox/mimetype_icons"
  241. isizes=22-64 # exactly two minus-separated icon sizes: smallest-largest
  242. execute_and_update=0 # execute and update
  243. allentries=0 # counting all entries in recentfile...
  244. unreadable=0 # ... of which some are unreadable
  245. # initialise a global RETVAL to be used in functions - avoids opening a subshell
  246. # https://rus.har.mn/blog/2010-07-05/subshells/
  247. RETVAL=''
  248. while getopts "dcs:m:r:g:i:nfx:XOh" opt; do
  249. case $opt in
  250. d) _debug() {
  251. echo "${*}(B" >&2
  252. return 0
  253. }
  254. ;;
  255. c)
  256. cat <<EOF > "$recentfile"
  257. <?xml version="1.0" encoding="UTF-8"?>
  258. <xbel version="1.0"
  259. xmlns:bookmark="http://www.freedesktop.org/standards/desktop-bookmarks"
  260. xmlns:mime="http://www.freedesktop.org/standards/shared-mime-info"
  261. >
  262. </xbel>
  263. EOF
  264. exit
  265. ;;
  266. s) isizes="$OPTARG"
  267. ;;
  268. m) [[ "$OPTARG" =~ [0-9]+ ]] && (( OPTARG >= 0 )) && (( OPTARG <= 65535 )) || _usage "Option -${opt}: invalid number $OPTARG"
  269. max="$OPTARG"
  270. ;;
  271. r) [ -r "$OPTARG" ] || _usage "Option -${opt}: Cannot read $OPTARG"
  272. recentfile="$OPTARG"
  273. ;;
  274. g) [ -r "$OPTARG" ] || _usage "Option -${opt}: Cannot read $OPTARG"
  275. gtksettingsfile="$OPTARG"
  276. ;;
  277. i) [[ "$OPTARG" == /* ]] || _usage "Option -${opt}: Invalid path $OPTARG - must be absolute"
  278. icondir="$OPTARG"
  279. ;;
  280. n) useicons=0
  281. ;;
  282. f) fullpath=0
  283. ;;
  284. x) if [ -n "$OPTARG" ]; then
  285. type "$OPTARG" >/dev/null 2>&1 || _usage "Option -${opt}: $OPTARG is not in PATH"
  286. mimeopener="$OPTARG"
  287. else
  288. mimeopener=""
  289. fi
  290. ;;
  291. X) # execute command & update recentfile
  292. execute_and_update=1
  293. ;;
  294. O) # only get an icon
  295. only_get_icon=1
  296. ;;
  297. h) _usage
  298. ;;
  299. *) _usage "Invalid option -$OPTARG"
  300. ;;
  301. esac
  302. done
  303. shift $((OPTIND-1))
  304. # called by the pipemenu itself to execute commands and simultaneously update $recentfile
  305. if [[ "$execute_and_update" == 1 ]]; then
  306. [ -t 0 ] && echo "This option should only be called by the pipemenu itself." && exit 1
  307. item="$1"; shift
  308. recentfile="$1"; shift
  309. "$@" & # executing command
  310. # Now updating recently-used file
  311. # date format: xbel.sourceforge.net/language/versions/1.0/xbel-1.0.xhtml and www.w3.org/TR/NOTE-datetime
  312. TZ=UTC printf -v time "%(%FT%R:%S.499999Z)T" # bash doesn't deal with fractions of seconds, they have been replaced with 499999
  313. tmp="$(xmlstarlet ed -u "/xbel/bookmark[@href=\"$item\"]/@visited" -v "$time" "$recentfile")"
  314. [[ "$tmp" != "" ]] && echo "$tmp" > "$recentfile"
  315. exit
  316. fi
  317. # a few things that are required only when you use icons:
  318. if [[ "$useicons" == 1 ]]; then
  319. icondirlist_file="$icondir/icondirlist"
  320. icondirlist=() # global list of icn directories to search for mimetypes
  321. I=0 # global counter for icondirlist
  322. mimequery=( mimes mimetypes apps places )
  323. deleteicon="user-trash,user-trash-symbolic"
  324. unknownicon="unknown,stock_unknown,gnome-unknown"
  325. [[ "$only_get_icon" == 1 ]] && { _loaddirlist; echo "$1"; echo "$2"; _geticon "$1"; echo "$RETVAL"; exit; }
  326. fi
  327. # quick dependency check
  328. type xmlstarlet >/dev/null || usage "Dependency missing: $x"
  329. # Use an Xpath expression to collect all
  330. # 1) file:///...
  331. # 2) application
  332. # elements, sorted by last modified date. Read into array and limit to $max elements.
  333. 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")"
  334. [[ "${array_raw[*]}" == "" ]] && _usage "No recent files."
  335. allentries="$(( ${#array_raw[@]} - 1))"
  336. [ -z "$mimeopener" ] || type "$mimeopener" >/dev/null 2>&1 || mimeopener="" >&2
  337. array_file=()
  338. array_cmd=()
  339. # start counting at 1 because the first array element is always an empty line due to the xmlstarlet command
  340. for ((i=1,j=0;i<${#array_raw[@]};i++)); do
  341. (( j >= max )) && break
  342. _debug "Raw line: ${array_raw[i]}"
  343. oldifs="$IFS"; IFS="$sep"
  344. line=(${array_raw[i]})
  345. IFS="$oldifs"
  346. # filepath - remove file:// and quotes, then decode both XML escapes and URL encoding
  347. _decode "${line[0]#*file://}"
  348. file="$RETVAL"
  349. # drop unreadable files here
  350. [ -r "$file" ] || { _debug "Unreadable: $file"; ((unreadable++)); continue; }
  351. ##########################
  352. _debug "Readable: ${line[0]}${sep}$file"
  353. array_file[j]="${line[0]}${sep}$file"
  354. for ((k=1;k<${#line[@]};k++)); do
  355. cmd="${line[k]}"
  356. # command to open file
  357. # remove single quotes, spaces and %u
  358. cmd="${cmd#*\'}"
  359. cmd="${cmd% %u\'}"
  360. type "${cmd%% *}" >/dev/null 2>&1 && array_cmd[j]="$cmd,${array_cmd[j]}"
  361. done
  362. # append $mimeopener, if defined, otherwise remove trailing comma
  363. [ -n "$mimeopener" ] && array_cmd[j]="${array_cmd[j]}$mimeopener" || array_cmd[j]="${array_cmd[j]%,}"
  364. _debug "Command: ${array_cmd[j]}"
  365. ((j++))
  366. done
  367. unset array_raw
  368. # without icons, that is all. Otherwise:
  369. if [[ "$useicons" == 1 ]]; then
  370. type gio >/dev/null || usage "Dependency missing for icons: gio"
  371. array_icon=()
  372. _useicons
  373. fi
  374. # make the pipemenu
  375. echo "<openbox_pipe_menu>"
  376. for ((i=0;i<${#array_file[@]};i++)); do
  377. # label of the menu entry - openbox-specific
  378. label="${array_file[i]#*$sep}"
  379. _debug "whole line: ${array_file[i]} - label: $label"
  380. [[ "$fullpath" == 1 ]] && label="${label/$HOME/\~}" || label="${label##*/}"
  381. # encode for XML
  382. # ampersands must be changed first:
  383. label="${label//&/&amp;}"
  384. label="${label//\"/&quot;}"
  385. label="${label//</&lt;}"
  386. label="${label//>/&gt;}"
  387. label="${label//_/__}"
  388. _debug "Label: ${label}"
  389. echo -n "<menu id=\"$label $i\" label=\"$label\""
  390. # add icon if applicable
  391. if [[ "$useicons" == 1 ]]; then
  392. #~ [ -d "${array_file[i]#*$sep}" ] &&
  393. _geticon "${array_icon[i]}"
  394. echo -n " icon=\"$RETVAL\""
  395. fi
  396. echo ">" # finalise the opening menu tag
  397. # command(s) to execute on the file
  398. oldifs="$IFS"; IFS=","
  399. cmd=(${array_cmd[i]})
  400. IFS="$oldifs"
  401. for c in "${cmd[@]}"; do
  402. exelabel="Open with $c"
  403. # escaping single quotes in the file path: stackoverflow.com/a/1315213
  404. exe="${array_file[i]#*${sep}}"
  405. exe="${exe//\'/\'\\\'\'}"
  406. #~ exe="'$c' '$exe'"
  407. _debug "Execute: $c $exe"
  408. echo "<item label=\"$exelabel\"><action name=\"Execute\"><command><![CDATA['$0' '-X' \"${array_file[i]%%${sep}*}\" \"$recentfile\" $c '$exe']]></command></action></item>"
  409. done
  410. echo "</menu>"
  411. done
  412. _endmenu