#!/bin/sh

###############################################################################
# i v1.8                                                                      #
# Get a lot of info about a file                                              #
# Copyright © JakeSFR 2024,2026                                               #
# License: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html             #
###############################################################################

export TEXTDOMAIN=i
export OUTPUT_CHARSET=UTF-8

APPVERSION="1.8"
APPNAME="i v${APPVERSION}"
COPYRIGHT="Copyright © JakeSFR 2024,2026"
LICENSE="GNU GPL v2 applies"

# =============================================================================

# Tricks and traps
trap 'f_wait_stop' EXIT
trap 'f_trap' INT HUP TERM QUIT ABRT
trap '' TTOU	# don't stop, when backgrounded

# -----------------------------------------------------------------------------

# void
f_trap() {
	f_wait_stop

	stty echo </dev/tty	# re-enable echoing
	printf -- "\n${IREDCOL}---%s---${OFFCOL}\n\n" "$(gettext "INTERRUPTED")"

	if [ "$I_GUI" ]; then
		[ "$YAD_PID" ] && kill -0 ${YAD_PID} 2>/dev/null && kill ${YAD_PID}
		[ "$YAD_KEY" ] && pkill -f "yad.*--(key|plug)=${YAD_KEY} "
		[ "$YAD_KEY" ] && ipcrm -M ${YAD_KEY} 2>/dev/null
	fi

	exit 255
}

# -----------------------------------------------------------------------------

# $1 = "$@"
f_click_or_dnd() {
	local exec_terminal exec_cmd opts f

	# If not started from terminal, use YAD GUI
	if ([ ! -t 0 ] || [ "$I_GUI" ]) && f_exists "$YAD_BIN" && "$YAD_BIN" --help-notebook >/dev/null 2>&1; then
		I_GUI='true'	# not set otherwise, so can be used as ENV variable

		if [ "$*" ]; then
			return
		else	# stock window-icon was 'gtk-dialog-info'
			if f="$("$YAD_BIN" \
				--title="$YAD_APPNAME" \
				--window-icon="$YAD_ICON" \
				--width=$(hidpi_apply_scale 640) --height=$(hidpi_apply_scale 480) \
				--center \
				--text "<big><b>$(gettext "Select input file or folder")</b></big>" \
				--text-align=center --file)"
			then
				exec "$(readlink -f "$0")" "$f"
			else
				exit
			fi
		fi
	fi

	# If not started from terminal, launch in terminal (fallback if YAD is unavailable)
	if ([ ! -t 0 ] || [ "$I_GUI" ]); then
		unset I_GUI	# unset, to avoid infinite loop if there's no YAD

		# I_TERM would be an ENV variable
		for exec_terminal in $I_TERM $TERMINAL defaultterm x-terminal-emulator mate-terminal gnome-terminal konsole xfce4-terminal terminator lxterminal roxterm urxvt rxvt mrxvt termit Eterm aterm uxterm termite sakura xterm vte terminology st qterminal lilyterm tilix terminix kitty alacritty hyper wezterm rio guake tilda; do
			f_exists "$exec_terminal" && break
		done

		case "$exec_terminal" in
			xfce4-terminal|mate-terminal)	exec_cmd='-x'	;;
			*)								exec_cmd='-e'	;;
		esac

		# If used, full options are required, otherwise some of them (like -p, -o) are "eaten" by the shell (?!)
		[ "$*" ] && opts="" || opts=""

		exec ${exec_terminal} -T "${APPNAME}" ${exec_cmd} "${SHELL}" -c '"$(readlink -f "$0")" "$@"; '"${SHELL}"'' "$(readlink -f "$0")" ${opts} "$@"
	fi
}

# -----------------------------------------------------------------------------

# $* = error message
f_error() {
	printf -- "${REDCOL}%s${OFFCOL} %s\n" "$(gettext "ERROR:")" "$*"
}

# -----------------------------------------------------------------------------

# void
f_help() {
	printf "%b\n"			"${UGREENCOL}${APPNAME} (${COPYRIGHT}; ${LICENSE})${OFFCOL}"
	printf "%b\n"			"${YELLOWCOL}$(gettext "Get a lot of info about a file.")${OFFCOL}\n"
	printf "%s\n\n"			"$(gettext "Usage:") ${0##*/} [options] <item>"
	printf "%s\n"			"$(gettext "Options:")"
	printf "  %-22s%s\n"	"-n, --no-color"	"$(gettext "disable ANSI colors in output")"
	printf "  %-22s%s\n"	"-r, --recursive"	"$(gettext "recurse subdirectories")"
	printf "  %-22s%s\n"	"-s, --short"		"$(gettext "suppress excessive output from some commands")"
	printf "  %-22s%s\n"	"-t, --trim"		"$(gettext "trim every line's length to terminal's width")"
	printf "\n%s\n"			"$(gettext 'Extra options:')"
	printf "  %-22s%s\n"	"-b, --backends"	"$(gettext "check availability of backends")"
	printf "  %-22s%s\n"	"-h, --help"		"$(gettext "this help")"
	printf "\n"
}

# -----------------------------------------------------------------------------

# $1 = character
f_hr() {
	printf -- "${1}%.0s" $(seq 1 $(eval ${TERM_WIDTH_CMD}))
}

# -----------------------------------------------------------------------------

# $1 = command
f_exists() {
	type "$1" >/dev/null 2>&1
}

# -----------------------------------------------------------------------------

# void
f_backends() {
	local cmd

	printf "${IYELLOWCOL}\n---%s---${OFFCOL}\n\n"	"$(gettext 'BACKENDS')"

	sed -n "s_[[:blank:]]\+[^#]*)[[:blank:]]\+cmd='\+\([[:graph:]]\+[^']\)'*[[:blank:]]\+.*_\1_p" "$0" | \
	tr '\n' ' ' | { read -r cmd; eval "printf '%s\n' $cmd"; } | sort | \
	while read cmd; do
		printf "${BOLDCOL}> %-32s${OFFCOL}"	"$cmd"
		if f_exists "$cmd"; then
			printf -- "${GREENCOL}%s${OFFCOL}\n"	"[$(gettext 'OK')]"
		else
			printf -- "${REDCOL}%s${OFFCOL}\n"		"[$(gettext 'MISSING')]"
		fi
	done

	printf "${OFFCOL}\n"
}

# -----------------------------------------------------------------------------

# $1 = current command
f_wait_start() {
	local i

	# See in real-time what's being currently executed (flushed by cat to avoid glitches in pagers)
	[ "$1" ] && printf -- "${IGRAYCOL}---%s---${OFFCOL}" "$1" | cat >&2

	printf '\033[?25l' >&2	# disable cursor

	while :; do
		for i in '-' '\' '|' '/'; do
			printf "%s\b" "${i}" >&2
			sleep 0.25
		done
	done
}

# -----------------------------------------------------------------------------

# void
f_wait_stop() {
	if [ "$WAIT_PID" ] && kill -0 ${WAIT_PID} 2>/dev/null; then
		kill ${WAIT_PID} 2>/dev/null
		wait ${WAIT_PID} 2>/dev/null
	fi

	printf "\r\033[2K" >&2			# clear line
	printf '\033[?25h' >&2			# enable cursor
	printf '\033[?25h' > /dev/tty	# enable cursor directly (workaround for 'result={...} 2>/dev/null' later)
}

# -----------------------------------------------------------------------------

# $1 = item
f_strip_mntpt() {
	local path mntpt

	path="$(readlink -f -- "$(pwd)/$1")"	# pwd: readlink fails at some stuff in /proc, if invoked relatively
	mntpt="$path"

	# Get mountpoint
	if f_exists mountpoint; then
		while ! mountpoint -q -- "$mntpt"; do
			mntpt="$([ "$(dirname -- 2>/dev/null)" ] && dirname "$mntpt" || dirname -- "$mntpt")"
		done
	else
		mntpt="$(df -- "$(readlink -f -- "$1")" | tail -n 1 | grep -oE '[^ ]+$')"
	fi
	
	# Strip mountpoint
	echo "${path#${mntpt}}"
}

# -----------------------------------------------------------------------------

# $1 = item
f_get_dev() {
	df -- "$1" | tail -n 1 | cut -f1 -d ' '
}

# -----------------------------------------------------------------------------

# $1 = item
f_is_ntfs() {
	[ -b "$1" ] && return 1
	[ "$(lsblk -n -o FSTYPE "$(f_get_dev "$1")" 2>/dev/null)" = 'ntfs' ]
}

# -----------------------------------------------------------------------------

# $1 = item
f_is_img() {
	LC_ALL=C fdisk -l -- "$1" 2>&1 | grep -q '^Device'
}

# -----------------------------------------------------------------------------

# $1 = item
f_appimage_offset() {
	local offset

	case "$(echo "$1" | tr '[:upper:]' '[:lower:]')" in
		*appimage)
			offset=$(LC_ALL=C grep -abo -m2 'hsqs' "$1" | sed -n 's/:.*//;2p') # 2nd 'hsqs' is "the one"
			;;
	esac

	echo ${offset:-0}
}

# -----------------------------------------------------------------------------

# pipe
f_pretty_xml() {
	if f_exists xmllint; then
		xmllint --nonet --format -
	else
		sed 's#\(</[^>]*>\|<[^>]*/>\)#&\n#g; s#><\([^/]\)#>\n<\1#g'
	fi
}

# -----------------------------------------------------------------------------

# $1 = filename, $2 = format, $3 = offset, $4 = amount
f_get_magic() {
	([ ! -f "$1" ] && [ ! -b "$1" ]) && return
	od -An -t${2} -j${3} -N${4} -- "$1" 2>/dev/null | tr -d ' '
}

# -----------------------------------------------------------------------------

# $1 = filename, $2 = ext1|ext2|...
f_check_ext() {
	eval '
		case "$(echo "${1##*.}" | tr "[:upper:]" "[:lower:]")" in
			'${2}')	return 0	;;
			*)		return 1	;;
		esac
	'
}

# -----------------------------------------------------------------------------

# $1 = item
f_i() {
	local i cmd flt trm result tabnum tabs fn_esc cmd_esc magic wrap

	fn_esc="$(echo "$1" | sed 's#\\#\\\\#g')"	# for YAD, needs to be escaped

	printf "\n${BLUECOL}$(f_hr "=")${OFFCOL}\n"
	printf -- "${BOLDCOL}%s${OFFCOL}" "${1}"
	printf "\n${BLUECOL}$(f_hr "=")${OFFCOL}\n"

	# Non-existing item
	if [ ! -e "$1" ] && [ ! -h "$1" ]; then
		printf "\n${YELLOWCOL}%s${OFFCOL}\n" "$(gettext "File doesn't exist, ignoring...")"
		return 1
	fi

	if [ "$I_GUI" ]; then
		YAD_KEY=$(expr $(tr -cd '0-9' < /dev/urandom | head -c 5) % 32768 + 1)	# can't be 0!
		tabnum=1
		tabs=''

		while :; do echo; sleep 0.2; done | \
			"${YAD_BIN}" --window-icon="$YAD_ICON" --title="$YAD_APPNAME" \
			--no-escape --fixed --center --text="<b>$(gettext "Please wait...")</b>" \
			--text-align=center --button="$(gettext "_Abort")!gtk-cancel:1" \
			--buttons-layout="center" --progress --pulsate --progress-text="" --hide-text --auto-kill &
		YAD_PID=$!
	fi

	# Get default magic and replace non-printable chars with spaces
	if ([ -f "$1" ] && [ -s "$1" ]) || [ -b "$1" ]; then
		magic="$(head -c 12 -- "$1" 2>/dev/null | tr -c ' -~' ' ')"
	else
		magic=''
	fi

	# cd to target dir, so tools don't display full path, which is distracting
	cd -- "$([ "$(dirname -- 2>/dev/null)" ] && dirname "$1" || dirname -- "$1")"
	set -- "$([ "$(basename -- 2>/dev/null)" ] && basename "$1" || basename -- "$1")"

	stty -echo </dev/tty	# disable echoing
	echo	# initial newline

	# @ = suitable for processing dirs and/or special files and regular/empty files and links
	# % = suitable for processing block devices and regular files and links only
	#   = suitable for regular files and links only
	for i in @FILE @XDG-MIME @ROX @LS @DU @STAT @GETFACL @CHACL @LSATTR @GETFATTR \
		@FATATTR @EXFATATTRIB @NTFSSECAUDIT @FIND @LSOF @MOUNTPOINT @DF @FINDMNT \
		\
		%MOUNT %LOSETUP %GUESS_FSTYPE %BLKID %LSBLK %FDISK %HDPARM %SDPARM \
		%SMARTCTL %SKDUMP %UDEVADM %E2FREEFRAG %DUMPE2FS %BTRFS %DOSFSCK %DUMP.EXFAT \
		%DUMPEXFAT %NTFSCLUSTER %NTFSINFO %XFS_INFO %DUMP.F2FS %CRYPTSETUP %HEXDUMP \
		\
		FILEFRAG WC FEH RDJPGCOM EXIFTOOL EXIV2 GIFSICLE TIFFDUMP \
		TIFFINFO WEBPNG WEBPINFO XCFINFO XCUR2PNG FC-SCAN FTDUMP HB-INFO \
		MPG123-ID3DUMP OGGINFO METAFLAC SNDFILE-INFO SBCINFO FFPROBE MEDIAINFO \
		PDFINFO PDFIMAGES PDFFONTS PDFDETACH UNZIP TAR BSDTAR DPKG-DEB RPM \
		ISOINFO UDFINFO COMPSIZE WIMINFO UPX UNSQUASHFS GZIP XZ ZSTD ZIPINFO SEVENZ \
		SQLITE3 QEMU-IMG PGPDUMP OPENSSL MODINFO READELF OBJDUMP LDD ZDUMP CTORRENT \
		AXMLDEC SIDPLAYFP C1541
	do
		if [ "${i%${i#?}}" = '@' ]; then	# @CMD
			i="${i#?}"
		elif [ "${i%${i#?}}" = '%' ]; then	# %CMD
			([ -d "$1" ] || [ -c "$1" ] || [ -p "$1" ] || [ -S "$1" ] || ([ -f "$1" ] && [ ! -s "$1" ])) && continue
			i="${i#?}"
		else
			([ -d "$1" ] || [ -b "$1" ] || [ -c "$1" ] || [ -p "$1" ] || [ -S "$1" ] || ([ -f "$1" ] && [ ! -s "$1" ])) && continue			
		fi

		# Exceptions/special cases
		case "$i" in
			DF|EXFATATTRIB|NTFSSECAUDIT)	# hang at processing broken symlinks; may try to read pipes
				( ([ ! -e "$1" ] && [ -h "$1" ]) || [ -p "$1" ] ) && continue
				;;
			FATATTR)	# hangs at some char devices; reads pipes
				([ -c "$1" ] || [ -p "$1" ]) && continue
				;;
			LSBLK)	# only block devices
				[ -b "$1" ] || continue
				;;
			FIND|MOUNTPOINT)	# only dirs
				[ ! -d "$1" ] && continue
				;;
			LSOF)	# don't process dirs
				[ -d "$1" ] && continue
				;;
			WC)	# should be used for plaintext files only
				grep -q "[^[:print:][:blank:]$(printf '\r')]" -- "$1" 2>/dev/null && continue
				;;
			OPENSSL)	# tends to hang for a long time on some files, so only supported
				head -c 4096  -- "$1" 2>/dev/null | grep -q '^-----BEGIN.*CERTIFICATE-----$' || continue
				;;
			TAR)	# would scan all archives, so only Fatdog/Slackware packages
				echo "$1" | grep -qw -- '.*-[0-9a-z_\.]\+-\(x86_64\|i[0-9]86\|noarch\)-\([0-9]\+\|official\).txz' || continue
				;;
			BSDTAR)	# reads other archives, so only DEB (ar)
				expr "$magic" : '!<arch>' >/dev/null || continue
				;;
			EXIV2)		# hangs at .gig files
				expr "$magic" : 'RIFF....DLS' >/dev/null && continue
				;;
			MPG123-ID3DUMP)	# processes all files; also, no exit codes
				expr "$magic" : 'ID3' >/dev/null || continue
				;;
			OGGINFO)	# tries to read other files - slow for big ones
				expr "$magic" : 'OggS' >/dev/null || continue
				;;
			PDFINFO|PDFIMAGES|PDFFONTS|PDFDETACH)	# ditto
				expr "$magic" : '%PDF' >/dev/null || continue
				;;
			SIDPLAYFP)	# ditto
				expr "$magic" : 'PSID\|RSID' >/dev/null || continue
				;;
			SQLITE3)	# ditto
				expr "$magic" : 'SQLite' >/dev/null || continue
				;;
			WEBPINFO)	# ditto
				expr "$magic" : 'RIFF....WEBP' >/dev/null || continue
				;;
			XCFINFO)	# ditto
				expr "$magic" : 'gimp.xcf' >/dev/null || continue
				;;
			XZ)	# ditto
				expr "$magic" : '.7zXZ' >/dev/null || continue
				;;
			ZIPINFO|AXMLDEC)	# zipinfo tries to read 7zip archives; axmldec reads big, non-apk files (slow)
				expr "$magic" : 'PK' >/dev/null || continue
				;;
			CRYPTSETUP)	# may modify ctime
				expr "$magic" : 'LUKS' >/dev/null || continue
				;;
			MODINFO|LDD)	# BB modinfo reads every file, same with ldd in Termux (because of --)
				expr "$magic" : '.ELF' >/dev/null || continue
				;;
			SNDFILE-INFO)	# no exit codes, so process only (some of) supported formats
				expr "$magic" : 'RIFF....WAVE\|riff\|fLaC\|ID3\|OggS\|FORM....AIFF\|.snd\|.paf\|Creative' >/dev/null || continue
				;;
			QEMU-IMG)	# processes everything, so only supported formats
				expr "$magic" : 'QFI\|KDMV\|<<< ' >/dev/null || continue
				;;
			ZDUMP)		# ditto
				expr "$magic" : 'TZif' > /dev/null || continue
				;;
			XFS_INFO)	# makes drive icon(s) to refresh, so limit only to actual XFS
				expr "$magic" : 'XFSB' >/dev/null || continue
				;;
			DUMP.EXFAT)	# ditto
				expr "$magic" : '...EXFAT' >/dev/null || continue
				;;
			DUMP.F2FS)	# ditto
				expr "$(f_get_magic "$1" x4 1024 4)" : 'f2f52010' >/dev/null 2>&1 || continue
				;;
			BTRFS)	# sometimes takes too long on unsupported partitions
				expr "$(f_get_magic "$1" a 65600 8)" : '_BHRfS_M' >/dev/null 2>&1 || continue
				;;
			DOSFSCK)	# process only actual FAT filesystems and ignore errors, so there's info, even if dirty bit is set
				expr "$(f_get_magic "$1" a 54 3)" : 'FAT' >/dev/null 2>&1 || continue
				;;
			FFPROBE)	# too many false-positives on GEM, .txt and .key files; accept all but these extensions
				f_check_ext "$1" 'img|txt|key' && continue
				;;
			PGPDUMP)	# too many false-positives, accept only these extensions
				f_check_ext "$1" 'sig|asc|pgp|gpg' || continue
				;;
			HB-INFO)	# ditto
				f_check_ext "$1" 'ttf|otf|ttc|otc|woff|woff2|dfont|pfa|pfb' || continue
				;;
			C1541)	# ditto
				f_check_ext "$1" 'd41|d64|d67|d71|d80|d81|d82|d90|g41|g64|g71|x64' || continue
				;;
		esac

		# Commands
		case "$i" in
			FILE)			cmd='file -bi -- "$1"; file -b -- "$1"'													;;
			XDG-MIME)		cmd='xdg-mime query filetype "$1"'														;;
			ROX)			cmd='rox -m "$1"'																		;;
			LS)				cmd='ls -lds -- "$1"'																	;;
			DU)				cmd='du -x -b -d 0 -- "$1"; du -x -b -h -d 0 -- "$1"; du -x -h -d 0 -- "$1"'			;;
			STAT)			cmd='stat -- "$1"'																		;;
			GETFACL)		cmd='getfacl -P -- "$1"'																;;
			CHACL)			cmd='chacl -l -- "$1"'																	;;
			LSATTR)			cmd='lsattr -d -- "$1"'																	;;
			GETFATTR)		cmd='getfattr -d -m ^ -- "$1"'															;;
			FATATTR)		cmd='fatattr "$1"'																		;;
			EXFATATTRIB)	cmd='exfatattrib -d "$(f_get_dev "$1")" -- "$(f_strip_mntpt "$1")"'						;;
			NTFSSECAUDIT)	cmd='ntfssecaudit "$1"; f_is_ntfs "$1"'													;;	# always gives an error - just don't if it's NTFS
			FIND)			cmd='find -H -- "$1" -xdev -printf "%y\n" | sort | uniq -c'								;;
			LSOF)			cmd='lsof -- "$1" | '${LSOF_FLT}''														;;
			MOUNTPOINT)		cmd='mountpoint -- "$1"'																;;
			DF)				cmd='df -h '${DF_OPTS}' -- "$1"'														;;
			FINDMNT)		cmd='findmnt -- "$1" || findmnt -T "$1"'												;;
			MOUNT)			cmd='mount | grep -F -- "/$1 "'															;;
			LOSETUP)		cmd='losetup -- "$1"'${LOSETUP_J}''														;;
			GUESS_FSTYPE)	cmd='guess_fstype -- "$1"'																;;
			BLKID)			cmd='blkid -- "$1"'																		;;
			LSBLK)			cmd='lsblk -f -m -t -J -- "$1"'															;;
			FDISK)			cmd='fdisk -l -- "$1"; f_is_img "$1"'													;;
			HDPARM)			cmd='hdparm -I -- "$1" | tail -n +4'													;;
			SDPARM)			cmd='sdparm -r -a -l -- "$1"'															;;
			SMARTCTL)		cmd='smartctl -x -- "$1"; [ $(($? & 3)) -eq 0 ]'										;;
			SKDUMP)			cmd='skdump -- "$1"'																	;;
			UDEVADM)		cmd='udevadm info --query=all --name="$(pwd -L)/$1"'									;;
			BTRFS)			cmd='btrfs filesystem show -- "$1"'														;;
			E2FREEFRAG)		cmd='e2freefrag -- "$1"'																;;
			DUMPE2FS)		cmd='dumpe2fs -h -- "$1"'																;;
			DOSFSCK)		cmd='dosfsck -n -v -- "$1";:'															;;
			DUMP.EXFAT)		cmd='dump.exfat "$1"'																	;;
			DUMPEXFAT)		cmd='dumpexfat -- "$1"'																	;;
			NTFSCLUSTER)	cmd='ntfscluster "$1"'																	;;
			NTFSINFO)		cmd='ntfsinfo -m "$1"'																	;;
			XFS_INFO)		cmd='xfs_info -- "$1"'																	;;
			DUMP.F2FS)		cmd='dump.f2fs -d 1 -- "$1"'															;;
			CRYPTSETUP)		cmd='cryptsetup -r luksDump -- "$1"'													;;
			HEXDUMP)		cmd='hexdump -C -- "$1" | head -n 1'													;;
			FILEFRAG)		cmd='filefrag -v -- "$1"';										flt="${FILEFRAG_FLT}"	;;
			WC)				cmd='wc -l -w -m -c -L -- "$1"'															;;
			FEH)			cmd='feh -l -- "$1"'																	;;
			RDJPGCOM)		cmd='rdjpgcom -verbose "$1"'															;;
			EXIFTOOL)		cmd='exiftool -- "$1"'																	;;
			EXIV2)			cmd='exiv2 -pc -- "$1"; exiv2 -- "$1"; exiv2 -pa -u -- "$1"';	flt="${EXIV2_FLT}"		;;
			GIFSICLE)		cmd='gifsicle -I --cinfo --xinfo --sinfo -- "$1"';				flt="${GIFSICLE_FLT}"	;;
			TIFFDUMP)		cmd='tiffdump -- "$1"'																	;;
			TIFFINFO)		cmd='tiffinfo -- "$1"';											flt="${TIFFINFO_FLT}"	;;
			WEBPNG)			cmd='webpng -dl -- "$1"'																;;
			WEBPINFO)		cmd='webpinfo -summary "$1"';									flt="${WEBPINFO_FLT}"	;;
			XCFINFO)		cmd='xcfinfo "$1"'																		;;
			XCUR2PNG)		cmd='xcur2png -n -- "$1" 2>/dev/null'													;;
			FC-SCAN)		cmd='fc-scan -- "$1"'																	;;
			FTDUMP)			cmd='ftdump "$1"'																		;;
			HB-INFO)		cmd='hb-info "$1"'																		;;
			MPG123-ID3DUMP)	cmd='mpg123-id3dump -n -- "$1"'															;;
			OGGINFO)		cmd='ogginfo -- "$1"'																	;;
			METAFLAC)		cmd='metaflac --list -- "$1"';									flt="${METAFLAC_FLT}"	;;
			SNDFILE-INFO)	cmd='sndfile-info "$1"'																	;;
			SBCINFO)		cmd='sbcinfo "$1"'																		;;
			FFPROBE)		cmd='ffprobe '${FFPROBE_OPTS}' -- "$1" 2>&1'											;;
			MEDIAINFO)		cmd='mediainfo -- "$1"'																	;;
			PDFINFO)		cmd='pdfinfo -- "$1"'																	;;
			PDFIMAGES)		cmd='pdfimages -list -- "$1"'															;;
			PDFFONTS)		cmd='pdffonts -- "$1"'																	;;
			PDFDETACH)		cmd='pdfdetach -list -- "$1"'															;;
			UNZIP)			cmd='unzip -p -- "$1" meta.xml docProps/core.xml 2>/dev/null | f_pretty_xml'			;;
			TAR)			cmd='tar -xOf "$1" '${TAR_OPTS}' install/slack-desc'									;;
			BSDTAR)			cmd='bsdtar -xOf "$1" control.tar.* | bsdtar -xOf - control'							;;
			DPKG-DEB)		cmd='dpkg-deb -f -- "$1"'																;;
			RPM)			cmd='rpm -qpi -- "$1"'																	;;
			ISOINFO)		cmd='isoinfo -d -i "$1"'																;;
			UDFINFO)		cmd='udfinfo -- "$1"'																	;;
			COMPSIZE)		cmd=''${COMPSIZE}' -- "$1"'																;;
			WIMINFO)		cmd='wiminfo -- "$1"'																	;;
			UPX)			cmd='upx -l -- "$1"'																	;;
			UNSQUASHFS)		cmd='unsquashfs '${UNSQUASH_OPTS}' -s "$1"'												;;
			GZIP)			cmd='gzip -l -- "$1"'																	;;
			XZ)				cmd='xz -l -- "$1"'																		;;
			ZSTD)			cmd='zstd -l -- "$1"'																	;;
			ZIPINFO)		cmd='zipinfo -htz -- "$1"'																;;
			SEVENZ)			cmd=''${SEVENZ}' l -p -slt -bse2 -bsp2 -- "$1" | sed -n "/^-\+/,/^-\+/ {/^-\+/d; p}"'	;;
			SQLITE3)		cmd='sqlite3 -readonly -deserialize "$1" .dbinfo'										;;	# deserialize excludes WAL DBs
			QEMU-IMG)		cmd='qemu-img info -- "$1"'																;;
			PGPDUMP)		cmd='pgpdump -- "$1"'																	;;
			OPENSSL)		cmd='openssl x509 -text -noout -passin pass: -in "$1"'									;;
			MODINFO)		cmd='modinfo -- "./$1"'																	;;
			READELF)		cmd='readelf -e -W -p .comment -- "$1"'													;;
			OBJDUMP)		cmd='objdump -x -- "$1"'																;;
			LDD)			cmd='ldd -- "$1"'																		;;
			ZDUMP)			cmd='zdump '${ZDUMP_OPTS}' -c $(($(date +%Y)-1)),$(($(date +%Y)+2)) -- "$(pwd -L)/$1"'	;;
			CTORRENT)		cmd='ctorrent -x -- "$1"'																;;
			AXMLDEC)		cmd='axmldec -i "$1"'																	;;
			SIDPLAYFP)		cmd='sidplayfp -v --none -t1 "$1" 2>&1 | sed "s/.*\r//; $ d"'							;;
			C1541)			cmd='c1541 "$1" -info'																	;;
			*)				continue
		esac

		f_exists "${cmd%% *}" || continue

		# Show the current command label and spinner
		f_wait_start "$i" & WAIT_PID=$!
		sleep 0.01	# without this delay, sometimes two labels are displayed next to each other

		# {...} 2>/dev/null is to get rid of "sh: ... ignored null byte in input" (exiv2 + asf/wma/wmv)
		if { result="$(eval ${cmd} 2>/dev/null)"; } 2>/dev/null && [ "$result" ]; then
			[ "$SHORT" != 'true' ] && flt='cat'

			if [ "$TRIM" ] && [ ${TRIM} -gt 0 ]; then
				#trm="sed -E '/^.{$(($(eval $TERM_WIDTH_CMD)*TRIM)),}/ s/^(.{$(($(eval $TERM_WIDTH_CMD)*TRIM-4))}).*/\1 .../'"
				trm="sed 's/^\(.\{$(($(eval $TERM_WIDTH_CMD)*TRIM-1))\}\).\{1,\}/\1$(printf "${INVCOL}>${OFFCOL}")/'"	# not as pedantic, but much faster!
			else
				trm='cat'
			fi

			# Remove blank and/or long whitespace-only lines and filter the result
			result="$(echo "$result" | tr -d '\0' | expand | \
				grep -v '^[[:space:]]*$' 2>/dev/null | \
				eval "$flt" | eval "$trm")"

			# Stop spinner
			f_wait_stop

			# Show result
			printf -- "${IBLUECOL}---%s---${OFFCOL}\n" "${i}"
			printf -- "${YELLOWCOL}# %s${OFFCOL}\n" "${cmd}"
			echo "$result"
			echo

			if [ "$I_GUI" ]; then
				cmd_esc="$(echo "$cmd" | sed 's#\\#\\\\#g')"	# for YAD, needs to be escaped

				# For very long lines disable wrapping, because it slows down YAD tremendously!
				if [ $(echo "$result" | wc -L | cut -f1 -d ' ') -gt 1024 ]; then
					wrap=''
				else
					wrap='--wrap'
				fi

				echo "$result" | \
					"${YAD_BIN}" --plug=${YAD_KEY} --tabnum=${tabnum} \
					--no-markup --text="$cmd_esc" --fontname='Monospace' \
					${wrap} --text-info - 2>/dev/null &

				tabnum=$((tabnum+1))
				tabs="${tabs}--tab=$(echo "$i" | sed 's/_/__/g') "	# YAD is changing _ to | in tab names
			fi
		else
			f_wait_stop	# stop spinner
		fi

		WAIT_PID=''
	done

	stty echo </dev/tty	# re-enable echoing

	cd - >/dev/null	# go back to previous dir

	if [ "$I_GUI" ]; then
		kill -0 ${YAD_PID} 2>/dev/null && kill -s USR1 ${YAD_PID}
		YAD_PID=''

		"${YAD_BIN}" --window-icon="$YAD_ICON" --title="$YAD_APPNAME" \
			--width=$(hidpi_apply_scale 800) --height=$(hidpi_apply_scale 480) \
			--center --button="$(gettext "_Close")!window-close" --buttons-layout="center" \
			--escape-ok --no-markup --text="$fn_esc" --text-align="center" \
			--notebook --key=${YAD_KEY} ${tabs} --tab-pos=left --active-tab=1

		pkill -f "yad.*--(key|plug)=${YAD_KEY} "	# kill zombie processes, if any
		YAD_KEY=''
	fi
}

# =============================================================================
# START
# =============================================================================

# Globals
YAD_APPNAME="i[nfo] v${APPVERSION}"
YAD_ICON="/usr/share/pixmaps/i.png"
YAD_BIN="${YAD_BIN:-yad}"
YAD_VERSION="$("$YAD_BIN" --version 2>/dev/null)"
YAD_KEY=''
YAD_PID=''
WAIT_PID=''

SHORT='false'
TRIM=0
RECURSIVE='false'

# Colors
OFFCOL="\033[0m"			# reset
BOLDCOL="\033[1m"			# bold
INVCOL="\033[7;1m"			# inverted
REDCOL="\033[0;31m"			# red
IREDCOL="\033[7;31m"		# inverted red
BLUECOL="\033[0;34m"		# blue
IBLUECOL="\033[7;34m"		# inverted blue
GREENCOL="\033[0;32m"		# green
YELLOWCOL="\033[0;33m"		# yellow
IYELLOWCOL="\033[7;33m"		# inverted yellow
IGRAYCOL="\033[7;90m"		# inverted gray
UGREENCOL="\033[4;32m"		# underlined green
UNSETCOLS="unset OFFCOL BOLDCOL INVCOL REDCOL IREDCOL BLUECOL IBLUECOL GREENCOL YELLOWCOL IYELLOWCOL IGRAYCOL BYELLOWCOL UGREENCOL"

# -----------------------------------------------------------------------------

# Adapt dimensions to hi-dpi screens (Fatdog)
if [ "${YAD_VERSION#*GTK+ 2}" != "$YAD_VERSION" ] || [ "${GDK_SCALE:-1}" = 1 ] && f_exists fatdog-hidpi-lib
then
	. fatdog-hidpi-lib
else
	hidpi_apply_scale() { echo "$@"; }
fi

# -----------------------------------------------------------------------------

# Add stuff from stdin (pipe and < redirection), if any
if [ ! -t 0 ] && read -r i; then					# only if stdin is NOT a terminal AND has data
	set --  "$@" "$i"								# restore first arg
	while IFS= read -r i; do set -- "$@" "$i"; done	# get the list
	exec </dev/tty									# restore stdin
fi

f_click_or_dnd "$@"	# must be _after_ the above stdin read!

# -----------------------------------------------------------------------------

# If gettext is missing (it's not given!), use 'echo' wrapper
if ! f_exists gettext; then
	gettext() { echo "$@"; }
fi

# Check if tput is available and fallback to stty if not
if f_exists tput; then
	TERM_WIDTH_CMD='tput cols'
else
	TERM_WIDTH_CMD='stty size | cut -f2 -d " "'
fi

# Older versions of ffmpeg/ffprobe don't have this option
if LC_ALL=C ffprobe -h 2>&1 | grep -iwoq -- '-hide_banner'; then
	FFPROBE_OPTS='-hide_banner'
else
	FFPROBE_OPTS=''
fi

# BusyBox lsof can only report _all_ open files, so we need a filter
if f_exists lsof && lsof --help 2>&1 | grep -qw BusyBox; then
	LSOF_FLT='grep -wF -- "$1"'
else
	LSOF_FLT='cat'
fi

# BusyBox losetup doesn't have the '-j' option
if f_exists losetup && losetup --help 2>&1 | grep -qw BusyBox; then
	LOSETUP_J=''
else
	LOSETUP_J='; losetup -j "$1"'
fi

# BusyBox tar doesn't support the '--occurence' option
if tar --help 2>&1 | grep -qw BusyBox; then
	TAR_OPTS=''
else
	TAR_OPTS='--occurrence=1'
fi

# Toybox df doesn't support the '-T' option
if df --help 2>&1 | grep -qw Toybox; then
	DF_OPTS=''
else
	DF_OPTS='-T'
fi

# SquashFS-tools up to 4.4 don't support the offset (-o) option
if unsquashfs -help-all 2>&1 | grep -qF -- '-o[ffset]'; then
	UNSQUASH_OPTS='-o $(f_appimage_offset "$1")'
else
	UNSQUASH_OPTS=''
fi

# Older version don't support the -i option, so use -V instead
if zdump --help 2>&1 | grep -qF -- '-i'; then
	ZDUMP_OPTS='-i'
else
	ZDUMP_OPTS='-V'
fi

# Pick the best 7zip
SEVENZ=7zz	# default
for i in 7zz 7zzs 7z 7za 7zr; do
	f_exists "$i" && { SEVENZ="$i"; break; }
done

# Fatdog has 'compsize' renamed to 'btrfs-compsize'
if f_exists btrfs-compsize; then
	COMPSIZE='btrfs-compsize'
else
	COMPSIZE='compsize'
fi

# Filters for truncating excessive output
EXIV2_FLT='sed "s/^\(\([^[:blank:]]\+[[:blank:]]\+\)\{3\}\([0-9]\+ \)\{16\}\)[0-9 ]\+/\1.../"'
TIFFINFO_FLT='sed "/: 0x[0-9a-fA-F]/ s/\( \(0x[0-9a-fA-F]\+,\)\{16\}\).*/\1 .../"'
WEBPINFO_FLT='sed -n "/^Summary/,$ p"'
GIFSICLE_FLT='sed "1,2b;d"'
METAFLAC_FLT="sed '/^[[:blank:]]*[0-9A-F]\{8\}: /d'"
FILEFRAG_FLT='sed "1,2b;$ b;d"'

# -----------------------------------------------------------------------------

# Honor NO_COLOR env variable, and a custom one, too
([ "$NO_COLOR" ] || [ "$I_NO_COLOR" ]) && ${UNSETCOLS}

case "$*" in
	''|-n|-no-color|--no-color|-no-colour|--no-colour)
		EXTRAOPT='help'
		;;
	*)
		EXTRAOPT=''
		;;
esac

# Parse options
while [ $# -gt 0 ]; do
	case "$1" in
		-n|-no-color|--no-color|-no-colour|--no-colour)
			${UNSETCOLS}
			shift
			;;
		-s|-short|--short)
			SHORT='true'
			shift
			;;
		-t|-trim|--trim)
			TRIM=1
			shift
			;;
		-trim=*|--trim=*)
			TRIM="${1#*=}"
			if ! [ "$TRIM" -eq "$TRIM" ] 2>/dev/null || [ ${TRIM} -lt 1 ] || [ ${TRIM} -gt 32 ]; then
				f_error "${1%=*}" "$(gettext "requires an integer argument <1-32>")"
				exit 1
			fi
			shift
			;;
		-r|-recursive|--recursive)
			RECURSIVE='true'
			shift
			;;
		-b|-backends|--backends)
			EXTRAOPT=backends
			shift
			;;
		-h|-help|--help)
			EXTRAOPT=help
			shift
			;;
		-[!-]?*)	# combined short options
			for i in $(seq 2 ${#1}); do
				opt="$(expr substr "$1" $i 1)"
				case "$opt" in
					n)	${UNSETCOLS}		;;
					s)	SHORT='true'		;;
					t)	TRIM=1				;;
					r)	RECURSIVE='true'	;;
					b)	EXTRAOPT=backends	;;
					h)	EXTRAOPT=help		;;
					*)	f_error "$(gettext "Unsupported option:")" "-$opt"
						exit 1
						;;
				esac
			done
			shift
		;;
		--)
			shift
			break
			;;
		-*)
			f_error "$(gettext "Unsupported option:")" "$1"
			exit 1
			;;
		*)
			break
			;;
	esac
done

case "$EXTRAOPT" in
	backends)	f_backends;	exit	;;
	help)		f_help;		exit	;;
esac

# -----------------------------------------------------------------------------

if [ $# -eq 0 ]; then
	printf "${YELLOWCOL}%s${OFFCOL}\n" "$(gettext "Nothing to process, exiting...")"
	exit
fi

# Main loop
while [ $# -gt 0 ]; do
	ITEM="$1"
	shift

	if [ -d "$ITEM" ] && [ "$RECURSIVE" = 'true' ]; then
		set -- "${ITEM%/}"/.* "$@"												# Add hidden items
		while [ "${1##*/}" = '.' ] || [ "${1##*/}" = '..' ]; do shift; done		# not needed in bash, only in (d)ash
		[ "${1%*/}" = "${ITEM%*/}/.*" ] && [ ! -e "${ITEM%*/}/.*" ] && shift	# empty dir
		set -- "${ITEM%/}"/* "$@"												# Add regular items
		[ "${1%*/}" = "${ITEM%*/}/*" ] && [ ! -e "${ITEM%*/}/*" ] && shift		# empty dir
	fi

	# Let's goooo!
	f_i "$ITEM"
done

printf "${BLUECOL}%s${OFFCOL}\n\n" "$(f_hr "*")"

exit

###############################################################################
