#!/bin/bash # [b]rowse - overview of given files or current directory # depends: tput stat bat checkaccess(in my dotfiles) # optdepends: timg, neovim (compressed files), pdftoppm (PDF), mtn (video), audiowaveform, imagemagick (images) # args: files to inspect, any arg starting with dash is passed on to bat # # Supports: # - listing directories # - listing contents of any compressed file with neovim # - visual files are displayed with timg # video thumbnails via mtn, pdf pages from pdftoppm # - text files are displayed through bat # Automatically requests elevation through sudo when needed set -o pipefail inspect=false opts='itvh' while getopts "$opts" OPTION; do case "$OPTION" in # inspect: Show file info without preview (i) inspect=true;; (t) tree=true;; (v) set -eo xtrace;; (h|?) echo "Usage: $(basename $0) [-$opts] " && exit 2;; esac done shift "$(($OPTIND -1))" checkperm() { checkaccess -r "$@" || elevate=sudo mime="$(test -n "$shifted" || $elevate file --dereference --mime "$@")" } fileinfo() { tput setaf 6 for arg do case "$arg" in (-*) continue;; esac $elevate file --exclude elf -E "$arg" $elevate ssh-keygen -l -f "$arg" 2>/dev/null || true # TODO do not grep bitrate but extract properly #probe="$($elevate ffprobe "$arg" 2>&1)" #echo $probe | grep -v -e '00:00:00.04' -e 'ansi' && $elevate ffprobe -hide_banner "$arg" 2>&1 | grep "bitrate: ....\? " | sed 's/, start:[^,]\+,/,/' || $elevate stat --format "%U:%G %A %s $( size="$($elevate unzip -l "$arg" 2>/dev/null | tail -1)" && echo "(uncompressed $(echo $size | cut -d' ' -f1 | numfmt --to=iec-i --suffix=B))" ) - birth %.10w mod %.10y" "$arg" | numfmt --field=3 --to=iec-i --padding=6 --suffix=B done tput sgr0 } prefix=/tmp/b mkdir -p "$prefix" declare -a timg timga bat batplain ls for arg; do case "$arg" in (-*) flags="$flags $arg"; continue;; esac checkperm "$arg" if ! $elevate test -e "$arg" then if test -h "$arg" then fileinfo "$arg" else echo "File not found: '$arg'" 1>&2 fi continue fi # amount of columns in a grid grid=$(expr $(tput cols) / \( 25 - \( $# / 2 \) \& $# \< 30 \| 5 \)) tmpfile="$prefix/$(basename "$arg")" mkdir -p "$prefix" case "$mime" in (*\ application/pdf\;*) echo Converting "$arg" grid=$(expr $(tput cols) \* $# / $(tput lines)) grid=$(expr 3 \& $grid \< 3 \| $grid) #limit=$(expr $grid \& \( $grid \> 3 \| $# \> 1 \) \| $grid '*' 2) limit=$grid test -f "$tmpfile-1.ppm" || pdftoppm -forcenum -r 70 "$arg" "$tmpfile" -l $limit find "$prefix" -path "$tmpfile*.ppm" | sort | head -$limit | xargs -d'\n' timg -W --grid=$grid ;; (*\ application/*document*|*.xlsx:\ *) # https://ask.libreoffice.org/t/convert-to-command-line-parameter/840/4 echo Converting "$arg" soffice --headless --convert-to png --outdir "$prefix" "$arg" >/dev/null timg+=("${tmpfile%.*}.png");; (*/x-xcf*) echo Converting "$arg" convert -flatten "$arg" png:"$tmpfile" timg+=("$tmpfile");; (*\ video/*) suffix=_thumbs.jpg mtn -q -i -t -W -r2 -D6 -b 0,6 -c $grid -w $(expr $(tput cols) '*' 20) \ -O "$prefix" -o "$suffix" "$arg" timg -W "$prefix/$(basename "${arg%.*}")$suffix" ;; (*\ image/*) timg+=("$arg") which identify >/dev/null && continue;; (*\ inode/directory\;*) ls+=("$arg") test -L "$arg" || continue ;; (*) case "$(file --dereference "$arg")" in (*\ ?udio*|*\ ADTS,*) # TODO preconvert aac - |*\ ADTS\ * if ! $inspect && which audiowaveform >/dev/null; then img="$tmpfile.png" case $TERM in (*-kitty) kitty=true; audioheight=2;; (*) audioheight=5;; esac find "$img" -not -empty 2>/dev/null | grep --quiet . || audiowaveform --quiet --pixels-per-second 2 --height 36 --width 2000 --amplitude-scale auto \ --background-color 000000 --waveform-color 99BBFF --axis-label-color 000000 \ --input-filename "$arg" --output-format png >"$img" && { test "$kitty" && timg -g x$audioheight "$arg" && printf "\\033[${audioheight}A%$(expr $audioheight \* 3)s" timg -g x$audioheight --auto-crop --upscale "$img" } fi timga+=("$arg") ;; (*:\ *compress*|*\ archive*) list="$tmpfile-list.txt" case "$arg" in (*.7z) test $# = 1 && 7z l "$arg";; (*.tar*) tar --list --file "$arg";; (*) if test $# = 1 then nvim "$arg" else case "$arg" in (*.part);; (*) nvim -es "+2w$list|5,w>>$list" "$arg" batplain+=("$list");; esac fi esac ;; (*:\ *database*) highlight "Tables" && sqlite3 "$arg" ".tables";; (*:\ data) ;; (*) bat+=("$arg") timga+=("$arg") continue ;; esac ;; esac fileinfo "$arg" done # timg: images # timga: potentially viewable as image if test "$timg"; then # TODO Don't show info on all images for gifs $inspect || if which timg >/dev/null then $elevate timg $(test "$timga" && echo "-V") --rotate=exif -g $(tput cols)x$(expr $(tput lines) / 2) \ $(test $# -gt 1 && echo "-t0.2 --center $(test $# -lt 20 && echo "--title") --grid=$((grid < $# ? grid : $#))x2") \ "${timg[@]}" "${timga[@]}" 2>/dev/null || true else for img in "${timg[@]}" do catimg -H $(expr $(tput lines) / 2) $img done fi if which identify >/dev/null && ( $inspect || test $# -lt 10 ); then tput setaf 6 for img in "${timg[@]}" do case $img in (*.gif) continue;; esac ident="$(identify -ping -precision 3 -format "%wx%h %b %m %[bit-depth]-bit %[colorspace]" "$img")" printf "%11s %-30s %s\n" "${ident%% *}" "$(basename "$img")" "${ident#* }" done tput sgr0 fi fi pager="${PAGER:-'less -RF'}" # bat: unknown files # batplain: files to print without header if test "$bat" -o "$batplain"; then test "$(bat --version | cut -d. -f2)" -gt 16 && rule=,rule if test $# -gt ${#bat[@]} -a $# -gt ${#batplain[@]} && test -z "$flags" then cut="--line-range :7" batpager="cut -c-$(expr $(tput cols) \* 19 / 10 | cut -d. -f1)" fi batcommand="$elevate bat $cut $flags --pager" batstyle="--style plain$rule" test "$batplain" && $batcommand "${batpager:-$pager}" $batstyle "${batplain[@]}" test "$bat" && if test "$cut" && ! $inspect then case $TERM in (*kitty) declare -a timgtxt for file in "${bat[@]}" do txt="$prefix/$(basename "$file").txt" cp "$file" "$txt" timgtxt+=("$txt") done timg -V --grid="$(expr 4 \& ${#bat[@]} \> 5 \| ${#bat[@]})" --title="%b" --frames=3 "${timgtxt[@]}" #fileinfo "${bat[@]}" ;; (*) for file in "${bat[@]}"; do $batcommand "${batpager:-$pager}" $batstyle "$file" fileinfo "$file" done;; esac else if ! $inspect then json=false if which cj >/dev/null; then json=true for file in "${bat[@]}"; do case $file in (*.json|*.geojson);; (*) json=false;; esac done fi if $json then cj "${bat[@]}" else $batcommand "${batpager:-$pager}" $batstyle,header$(test $# -gt 1 && echo ",numbers") "${bat[@]}" fi fi if $inspect || test $# -lt $(expr $(tput lines) / 3); then fileinfo "${bat[@]}" fi fi fi if test "$ls" -o $# -le $(echo "$flags" | wc -w); then checkperm . # Alternative: find -exec ls -dl {} + { # TODO handle single quotes in filenames # TODO listing sometimes doubles as exa prints partial listings while working timeout .6s sh -c " if test '$tree' then $elevate tree -a --dirsfirst --du -h -C -L 3 $flags $(printf "'%s' " "${ls[@]:-.}") elif which exa 2>/dev/null >&2 then $elevate exa --icons --color=always --long --group --classify --all --all --sort=changed --reverse $flags $(printf "'%s' " "${ls[@]:-.}") else $elevate ls -l $(test $# -gt ${#ls[@]} && echo '-d') --color=always --human-readable --si --group-directories-first --file-type --dereference-command-line-symlink-to-dir --all $flags $(printf "'%s' " "${ls[@]:-.}") fi " || $elevate ls $(test $# -gt ${#ls[@]} && echo "-d") --color=always --human-readable --si --dereference-command-line --all --sort=none $flags "${ls[@]:-.}" } | $pager --quit-if-one-screen fi