#!/usr/bin/env bash # Description: tabbed/xembed based file previewer # # Dependencies: # - tabbed (https://tools.suckless.org/tabbed): xembed host # - xterm (or urxvt or st) : xembed client for text-based preview # - mpv (https://mpv.io): xembed client for video/audio # - sxiv (https://github.com/muennich/sxiv): xembed client for images # - zathura (https://pwmt.org/projects/zathura): xembed client for PDF # - nnn's nuke plugin for text preview and fallback # nuke is a fallback for 'mpv', 'sxiv', and 'zathura', but has its # own dependencies, see the script for more information # - vim (or any editor/pager really) # - file # - mktemp # - xdotool (optional, to keep main window focused) # # Usage: # - Install the dependencies. Then set a NNN_FIFO # and set a key for the plugin, then start `nnn`: # $ NNN_FIFO=/tmp/nnn.fifo nnn # - Launch the plugin with the designated key from nnn # # Notes: # 1. This plugin needs a "NNN_FIFO" to work. See man. # 2. If the same NNN_FIFO is used in multiple nnn instances, there will be one # common preview window. With different FIFO paths, they will be independent. # # How it works: # We use `tabbed` [1] as a xembed [2] host, to have a single window # owning each previewer window. So each previewer must be a xembed client. # For text previewers, this is not an issue, as there are a lot of # xembed-able terminal emulator (we default to `xterm`, but examples are # provided for `urxvt` and `st`). For graphic preview this can be trickier, # but a few popular viewers are xembed-able, we use: # - `mpv`: multimedia player, for video/audio preview # - `sxiv`: image viewer # - `zathura`: PDF viewer # - but we always fallback to `nuke` plugin # # [1]: https://tools.suckless.org/tabbed/ # [2]: https://specifications.freedesktop.org/xembed-spec/xembed-spec-latest.html # # Shell: Bash (job control is weakly specified in POSIX) # Author: Léo Villeveygoux XDOTOOL_TIMEOUT=2 PAGER=${PAGER:-"vim -R"} NUKE="${XDG_CONFIG_HOME:-$HOME/.config}/nnn/plugins/nuke" if type urxvt >/dev/null 2>&1 ; then TERMINAL="urxvt -embed" elif type st >/dev/null 2>&1 ; then TERMINAL="st -w" else echo "No xembed term found" >&2 fi term_nuke () { # $1 -> $XID, $2 -> $FILE $TERMINAL "$1" -e "$NUKE" "$2" & } start_tabbed () { FIFO="$(mktemp -u)" mkfifo "$FIFO" tabbed > "$FIFO" & jobs # Get rid of the "Completed" entries TABBEDPID="$(jobs -p %%)" if [ -z "$TABBEDPID" ] ; then echo "Can't start tabbed" exit 1 fi read -r XID < "$FIFO" rm "$FIFO" } get_viewer_pid () { VIEWERPID="$(jobs -p %%)" } kill_viewer () { if [ -n "$VIEWERPID" ] && jobs -p | grep "$VIEWERPID" ; then kill "$VIEWERPID" fi } sigint_kill () { kill_viewer kill "$TABBEDPID" exit 0 } previewer_loop () { unset -v NNN_FIFO # mute from now exec >/dev/null 2>&1 MAINWINDOW="$(xdotool getactivewindow)" start_tabbed trap sigint_kill SIGINT xdotool windowactivate "$MAINWINDOW" # Bruteforce focus stealing prevention method, # works well in floating window managers like XFCE # but make interaction with the preview window harder # (uncomment to use): #xdotool behave "$XID" focus windowactivate "$MAINWINDOW" & while read -r FILE ; do jobs # Get rid of the "Completed" entries if ! jobs | grep tabbed ; then break fi if [ ! -e "$FILE" ] ; then continue fi kill_viewer MIME="$(file -bL --mime-type "$FILE")" case "$MIME" in video/*) if type mpv >/dev/null 2>&1 ; then mpv --force-window=immediate --loop-file --wid="$XID" "$FILE" & elif type ffplay >/dev/null 2>&1; then ffplay -window_title preview -loop 0 "$FILE" & elif type mplayer >/dev/null 2>&1 ; then mplayer -nocache -framedrop -vfm ffmpeg -lavdopts lowres=1:fast:skiploopfilter=all -loop 0 -wid "$XID" "$FILE" & elif type vlc >/dev/null 2>&1 ; then vlc --loop --drawable-xid="$XID" "$FILE" & else term_nuke "$XID" "$FILE" fi ;; audio/*) if type mpv >/dev/null 2>&1 ; then mpv --force-window=immediate --loop-file --wid="$XID" "$FILE" & elif type mplayer >/dev/null 2>&1 ; then mplayer -loop 0 -wid "$XID" "$FILE" & elif type vlc >/dev/null 2>&1 ; then vlc --loop --drawable-xid="$XID" "$FILE" & else term_nuke "$XID" "$FILE" fi ;; image/*) if type sxiv >/dev/null 2>&1 ; then sxiv -ae "$XID" "$FILE" & else term_nuke "$XID" "$FILE" fi ;; application/pdf) if type zathura >/dev/null 2>&1 ; then zathura -e "$XID" "$FILE" & else term_nuke "$XID" "$FILE" fi ;; inode/directory) $TERMINAL "$XID" -e nnn "$FILE" & ;; text/*) if [ -x "$NUKE" ] ; then term_nuke "$XID" "$FILE" else # shellcheck disable=SC2086 $TERMINAL "$XID" -e $PAGER "$FILE" & fi ;; *) if [ -x "$NUKE" ] ; then term_nuke "$XID" "$FILE" else $TERMINAL "$XID" -e sh -c "file '$FILE' | $PAGER -" & fi ;; esac get_viewer_pid # following lines are not needed with the bruteforce xdotool method ACTIVE_XID="$(xdotool getactivewindow)" if [ $((ACTIVE_XID == XID)) -ne 0 ] ; then xdotool windowactivate "$MAINWINDOW" else timeout "$XDOTOOL_TIMEOUT" xdotool behave "$XID" focus windowactivate "$MAINWINDOW" & fi done kill "$TABBEDPID" kill_viewer } if [ ! -r "$NNN_FIFO" ] ; then echo "Can't read \$NNN_FIFO ('$NNN_FIFO')" exit 1 fi previewer_loop < "$NNN_FIFO" & disown