#!/usr/bin/env bash
# -----------------------------------------------------------------------------
# bat-extras | Copyright (C) 2019-2020 eth-p | MIT License
#
# Repository: https://github.com/eth-p/bat-extras
# Issues:     https://github.com/eth-p/bat-extras/issues
# -----------------------------------------------------------------------------
# shellcheck disable=SC1090
# --- BEGIN LIBRARY FILE: opt.sh ---
SHIFTOPT_HOOKS=()
SHIFTOPT_SHORT_OPTIONS="VALUE"
setargs(){
_ARGV=("$@")
_ARGV_LAST="$((${#_ARGV[@]}-1))"
_ARGV_INDEX=0
_ARGV_SUBINDEX=1
}
getargs(){
if [[ $1 == "-a" || $1 == "--append" ]];then
if [[ $_ARGV_INDEX -ne "$((_ARGV_LAST+1))" ]];then
eval "$2=(\"\${$2[@]}\" $(printf '%q ' "${_ARGV[@]:_ARGV_INDEX}"))"
fi
else
if [[ $_ARGV_INDEX -ne "$((_ARGV_LAST+1))" ]];then
eval "$1=($(printf '%q ' "${_ARGV[@]:_ARGV_INDEX}"))"
else
eval "$1=()"
fi
fi
}
resetargs(){
setargs "${_ARGV_ORIGINAL[@]}"
}
_shiftopt_next(){
_ARGV_SUBINDEX=1
((_ARGV_INDEX++))||true
}
shiftopt(){
[[ $_ARGV_INDEX -gt $_ARGV_LAST ]]&&return 1
OPT="${_ARGV[$_ARGV_INDEX]}"
unset OPT_VAL
if [[ $OPT =~ ^-[a-zA-Z0-9_-]+=.* ]];then
OPT_VAL="${OPT#*=}"
OPT="${OPT%%=*}"
fi
if [[ $OPT =~ ^-[^-]{2,} ]];then
case "$SHIFTOPT_SHORT_OPTIONS" in
PASS)_shiftopt_next;;
CONV)OPT="-$OPT"
_shiftopt_next
;;
VALUE){
OPT="${_ARGV[$_ARGV_INDEX]}"
OPT_VAL="${OPT:2}"
OPT="${OPT:0:2}"
_shiftopt_next
};;
SPLIT){
OPT="-${OPT:_ARGV_SUBINDEX:1}"
((_ARGV_SUBINDEX++))||true
if [[ $_ARGV_SUBINDEX -gt ${#OPT} ]];then
_shiftopt_next
fi
};;
*)printf "shiftopt: unknown SHIFTOPT_SHORT_OPTIONS mode '%s'" \
"$SHIFTOPT_SHORT_OPTIONS" 1>&2
_shiftopt_next
esac
else
_shiftopt_next
fi
local hook
for hook in "${SHIFTOPT_HOOKS[@]}";do
if "$hook";then
shiftopt
return $?
fi
done
return 0
}
shiftval(){
if [[ -n ${OPT_VAL+x} ]];then
return 0
fi
if [[ $_ARGV_SUBINDEX -gt 1 && $SHIFTOPT_SHORT_OPTIONS == "SPLIT" ]];then
OPT_VAL="${_ARGV[$((_ARGV_INDEX+1))]}"
else
OPT_VAL="${_ARGV[$_ARGV_INDEX]}"
_shiftopt_next
fi
if [[ $OPT_VAL =~ -.* ]];then
printc "%{RED}%s: '%s' requires a value%{CLEAR}\n" "batwatch" "$ARG"
exit 1
fi
}
setargs "$@"
_ARGV_ORIGINAL=("$@")
# --- END LIBRARY FILE ---
# --- BEGIN LIBRARY FILE: opt_hook_color.sh ---
hook_color(){
SHIFTOPT_HOOKS+=("__shiftopt_hook__color")
__shiftopt_hook__color(){
case "$OPT" in
--no-color)OPT_COLOR=false;;
--color){
case "$OPT_VAL" in
"")OPT_COLOR=true;;
always|true)OPT_COLOR=true;;
never|false)OPT_COLOR=false;;
auto)return 0;;
*)printc "%{RED}%s: '--color' expects value of 'auto', 'always', or 'never'%{CLEAR}\n" "batwatch"
exit 1
esac
};;
*)return 1
esac
printc_init "$OPT_COLOR"
return 0
}
if [[ -z $OPT_COLOR ]];then
if [[ -t 1 ]];then
OPT_COLOR=true
else
OPT_COLOR=false
fi
printc_init "$OPT_COLOR"
fi
}
# --- END LIBRARY FILE ---
# --- BEGIN LIBRARY FILE: opt_hook_help.sh ---
hook_help(){
SHIFTOPT_HOOKS+=("__shiftopt_hook__help")
if [[ $1 == "--no-short" ]];then
__shiftopt_hook__help(){
if [[ $OPT == "--help" ]];then
show_help
exit 0
fi
return 1
}
else
__shiftopt_hook__help(){
if [[ $OPT == "--help" ]]||[[ $OPT == "-h" ]];then
show_help
exit 0
fi
return 1
}
fi
}
# --- END LIBRARY FILE ---
# --- BEGIN LIBRARY FILE: opt_hook_version.sh ---
hook_version(){
SHIFTOPT_HOOKS+=("__shiftopt_hook__version")
__shiftopt_hook__version(){
if [[ $OPT == "--version" ]];then
printf "%s %s\n\n%s\n%s\n" \
"batwatch" \
"2023.09.19" \
"Copyright (C) 2019-2021 eth-p | MIT License" \
"https://github.com/eth-p/bat-extras"
exit 0
fi
return 1
}
}
# --- END LIBRARY FILE ---
# --- BEGIN LIBRARY FILE: opt_hook_width.sh ---
term_width(){
local width="$({ stty size 2>/dev/null||echo "22 80";}|cut -d ' ' -f2)"
if [[ $width -ne 0 ]];then
echo "$width"
else
echo "80"
fi
return 0
}
term_clear(){
printf "\x1B[3J\x1B[2J\x1B[H"
}
hook_width(){
SHIFTOPT_HOOKS+=("__shiftopt_hook__width")
__shiftopt_hook__width(){
case "$OPT" in
--terminal-width)shiftval
OPT_TERMINAL_WIDTH="$OPT_VAL"
;;
*)return 1
esac
return 0
}
OPT_TERMINAL_WIDTH="$(term_width)"
}
# --- END LIBRARY FILE ---
# --- BEGIN LIBRARY FILE: print.sh ---
printc(){
printf "$(sed "$_PRINTC_PATTERN" <<<"$1")" "${@:2}"
}
printc_init(){
case "$1" in
true)_PRINTC_PATTERN="$_PRINTC_PATTERN_ANSI";;
false)_PRINTC_PATTERN="$_PRINTC_PATTERN_PLAIN";;
"[DEFINE]"){
_PRINTC_PATTERN_ANSI=""
_PRINTC_PATTERN_PLAIN=""
local name
local ansi
while read -r name ansi;do
if [[ -z $name && -z $ansi ]]||[[ ${name:0:1} == "#" ]];then
continue
fi
ansi="${ansi/\\/\\\\}"
_PRINTC_PATTERN_PLAIN="${_PRINTC_PATTERN_PLAIN}s/%{$name}//g;"
_PRINTC_PATTERN_ANSI="${_PRINTC_PATTERN_ANSI}s/%{$name}/$ansi/g;"
done
if [[ -t 1 && -z ${NO_COLOR+x} ]];then
_PRINTC_PATTERN="$_PRINTC_PATTERN_ANSI"
else
_PRINTC_PATTERN="$_PRINTC_PATTERN_PLAIN"
fi
}
esac
}
print_warning(){
printc "%{YELLOW}[%s warning]%{CLEAR}: $1%{CLEAR}\n" "batwatch" "${@:2}" 1>&2
}
print_error(){
printc "%{RED}[%s error]%{CLEAR}: $1%{CLEAR}\n" "batwatch" "${@:2}" 1>&2
}
printc_init "[DEFINE]" <<END
	CLEAR	\x1B[0m
	RED		\x1B[31m
	GREEN	\x1B[32m
	YELLOW	\x1B[33m
	BLUE	\x1B[34m
	MAGENTA	\x1B[35m
	CYAN	\x1B[36m

	DEFAULT \x1B[39m
	DIM		\x1B[2m
END
# --- END LIBRARY FILE ---
# --- BEGIN LIBRARY FILE: pager.sh ---
is_pager_less(){
[[ "$(pager_name)" == "less" ]]
return $?
}
is_pager_bat(){
[[ "$(pager_name)" == "bat" ]]
return $?
}
is_pager_disabled(){
[[ -z "$(pager_name)" ]]
return $?
}
pager_name(){
_detect_pager 1>&2
echo "$_SCRIPT_PAGER_NAME"
}
pager_version(){
_detect_pager 1>&2
echo "$_SCRIPT_PAGER_VERSION"
}
pager_exec(){
if [[ -n $SCRIPT_PAGER_CMD ]];then
"$@"|pager_display
return $?
else
"$@"
return $?
fi
}
pager_display(){
if [[ -n $SCRIPT_PAGER_CMD ]];then
if [[ -n $SCRIPT_PAGER_ARGS ]];then
"${SCRIPT_PAGER_CMD[@]}" "${SCRIPT_PAGER_ARGS[@]}"
return $?
else
"${SCRIPT_PAGER_CMD[@]}"
return $?
fi
else
cat
return $?
fi
}
_detect_pager(){
if [[ $_SCRIPT_PAGER_DETECTED == "true" ]];then return;fi
_SCRIPT_PAGER_DETECTED=true
if [[ -z ${SCRIPT_PAGER_CMD[0]} ]];then
_SCRIPT_PAGER_VERSION=0
_SCRIPT_PAGER_NAME=""
return
fi
local output
local output1
output="$("${SCRIPT_PAGER_CMD[0]}" --version 2>&1)"
output1="$(head -n 1 <<<"$output")"
if [[ $output1 =~ ^less[[:blank:]]([[:digit:]]+) ]];then
_SCRIPT_PAGER_VERSION="${BASH_REMATCH[1]}"
_SCRIPT_PAGER_NAME="less"
elif [[ $output1 =~ ^bat(cat)?[[:blank:]]([[:digit:]]+) ]];then
__BAT_VERSION="${BASH_REMATCH[2]}"
_SCRIPT_PAGER_VERSION="${BASH_REMATCH[2]}"
_SCRIPT_PAGER_NAME="bat"
else
_SCRIPT_PAGER_VERSION=0
_SCRIPT_PAGER_NAME="$(basename "${SCRIPT_PAGER_CMD[0]}")"
fi
}
_configure_pager(){
SCRIPT_PAGER_ARGS=()
if [[ -n ${PAGER+x} ]];then
SCRIPT_PAGER_CMD=($PAGER)
else
SCRIPT_PAGER_CMD=("less")
fi
if [[ -n ${BAT_PAGER+x} ]];then
SCRIPT_PAGER_CMD=($BAT_PAGER)
SCRIPT_PAGER_ARGS=()
return
fi
if is_pager_bat;then
SCRIPT_PAGER_CMD=("less")
SCRIPT_PAGER_ARGS=()
fi
if is_pager_less;then
SCRIPT_PAGER_CMD=("${SCRIPT_PAGER_CMD[0]}" -R --quit-if-one-screen)
if [[ "$(pager_version)" -lt 500 ]];then
SCRIPT_PAGER_CMD+=(--no-init)
fi
fi
}
if [[ -t 1 ]];then
_configure_pager
else
SCRIPT_PAGER_CMD=()
SCRIPT_PAGER_ARGS=()
fi
# --- END LIBRARY FILE ---
# --- BEGIN LIBRARY FILE: version.sh ---
bat_version(){
if [[ -z $__BAT_VERSION ]];then
__BAT_VERSION="$(command "bat" --version|cut -d ' ' -f 2)"
fi
echo "$__BAT_VERSION"
}
version_compare(){
local version="$1"
local compare="$3"
if ! [[ $version =~ \.$ ]];then
version="$version."
fi
if ! [[ $compare =~ \.$ ]];then
compare="$compare."
fi
version_compare__recurse "$version" "$2" "$compare"
return $?
}
version_compare__recurse(){
local version="$1"
local operator="$2"
local compare="$3"
local v_major="${version%%.*}"
local c_major="${compare%%.*}"
local v_minor="${version#*.}"
local c_minor="${compare#*.}"
if [[ -z $v_minor && -z $c_minor ]];then
[ "$v_major" $operator "$c_major" ]
return $?
fi
if [[ -z $v_minor ]];then
v_minor="0."
fi
if [[ -z $c_minor ]];then
c_minor="0."
fi
case "$operator" in
-eq)[[ $v_major -ne $c_major ]]&&return 1;;
-ne)[[ $v_major -ne $c_major ]]&&return 0;;
-ge|-gt)[[ $v_major -lt $c_major ]]&&return 1
[[ $v_major -gt $c_major ]]&&return 0
;;
-le|-lt)[[ $v_major -gt $c_major ]]&&return 1
[[ $v_major -lt $c_major ]]&&return 0
esac
version_compare__recurse "$v_minor" "$operator" "$c_minor"
}
# --- END LIBRARY FILE ---
# -----------------------------------------------------------------------------
# Init:
# -----------------------------------------------------------------------------
hook_color
hook_version
hook_width
hook_help
# -----------------------------------------------------------------------------
# Help:
# -----------------------------------------------------------------------------
show_help() {
	echo 'Usage: batwatch --file [--watcher entr|poll][--[no-]clear] <file> [<file> ...]'
	echo '       batwatch --command [-n<interval>] <command> [<arg> ...]' 
}
# -----------------------------------------------------------------------------
# Watchers:
# -----------------------------------------------------------------------------

WATCHERS=("entr" "poll")

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

watcher_entr_watch() {
	ENTR_ARGS=()

	if [[ "$OPT_CLEAR" == "true" ]]; then
		ENTR_ARGS+=('-c')
	fi

	entr "${ENTR_ARGS[@]}" \
		"bat" "${BAT_ARGS[@]}" \
		--terminal-width="$OPT_TERMINAL_WIDTH" \
		--paging=never \
		"$@" \
		< <(printf "%s\n" "$@")
}

watcher_entr_supported() {
	command -v entr &> /dev/null
	return $?
}

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

POLL_STAT_VARIANT=''
POLL_STAT_COMMAND=()

determine_stat_variant() {
	if [[ -n "$POLL_STAT_VARIANT" ]]; then
		return 0
	fi

	local variant name flags ts
	for variant in "gnu -c %Z" "bsd -f %m"; do
		read    -r name flags <<< "$variant"

		# save the results of the stat command
		if read -r ts < <(stat ${flags} "$0" 2> /dev/null); then

			# verify that the value is an epoch timestamp
			# before proceeding
			if [[ "${ts}" =~ ^[0-9]+$ ]]; then
				POLL_STAT_COMMAND=(stat ${flags})
				POLL_STAT_VARIANT="$name"
				return 0
			fi
		fi
	done

	return 1
}

watcher_poll_watch() {
	determine_stat_variant

	local files=("$@")
	local times=()

	# Get the initial modified times.
	local file
	local time
	local modified=true
	for file in "${files[@]}"; do
		time="$("${POLL_STAT_COMMAND[@]}" "$file")"
		times+=("$time")
	done

	# Display files.
	while true; do
		if "$modified"; then
			modified=false
			clear
			"bat" "${BAT_ARGS[@]}" \
				--terminal-width="$OPT_TERMINAL_WIDTH" \
				--paging=never \
				"${files[@]}"

		fi

		# Check if the file has been modified.
		local i=0
		for file in "${files[@]}"; do
			time="$("${POLL_STAT_COMMAND[@]}" "$file")"

			if [[ "$time" -ne "${times[$i]}" ]]; then
				times[$i]="$time"
				modified=true
			fi

			((i++))
		done

		# Wait for "q" to exit, or check again after a few seconds.
		local input
		read -r -t "${OPT_INTERVAL}" input
		if [[ "$input" =~ [q|Q] ]]; then
			exit
		fi
	done

	"${POLL_STAT_COMMAND[@]}" "$@"
	local ts
}

watcher_poll_supported() {
	determine_stat_variant
	return $?
}

# -----------------------------------------------------------------------------
# Functions:
# -----------------------------------------------------------------------------

determine_watcher() {
	local watcher
	for watcher in "${WATCHERS[@]}"; do
		if "watcher_${watcher}_supported"; then
			OPT_WATCHER="$watcher"
			return 0
		fi
	done

	return 1
}

# -----------------------------------------------------------------------------
# Options:
# -----------------------------------------------------------------------------
BAT_ARGS=(--paging=never)
FILES=()
FILES_HAS_DIRECTORY=false
OPT_MODE=file
OPT_INTERVAL=3
OPT_CLEAR=true
OPT_WATCHER=""

# Set options based on tty.
if [[ -t 1 ]]; then
	OPT_COLOR=true
fi

# Parse arguments.
while shiftopt; do
	case "$OPT" in

		# Script options
		--watcher)     shiftval;   OPT_WATCHER="$OPT_VAL" ;;
		--interval|-n) shiftval;   OPT_INTERVAL="$OPT_VAL" ;;
		--file|-f)                 OPT_MODE=file ;;
		--command|-x)              OPT_MODE=command ;;
		--clear)                   OPT_CLEAR=true ;;
		--no-clear)                OPT_CLEAR=false ;;

		# bat/Pager options
		-*) BAT_ARGS+=("$OPT=$OPT_VAL") ;;

		# Files
		*) {
			FILES+=("$OPT")
			if [[ "$OPT_MODE" = "command" ]]; then
				getargs --append FILES
				break
			fi
		} ;;

	esac
done

# Validate that a file/command was provided.
if [[ ${#FILES[@]} -eq 0 ]]; then
	if [[ "$OPT_MODE" = "file" ]]; then
		print_error "no files provided"
	else
		print_error "no command provided"
	fi
	exit 1
fi
	
# Validate that the provided files exist.
if [[ "$OPT_MODE" = "file" ]]; then
	for file in "${FILES[@]}"; do
		if ! [[ -e "$file" ]]; then
			print_error "'%s' does not exist" "$file"
			exit 1
		fi
	
		if [[ -d "$file" ]]; then
			FILES_HAS_DIRECTORY=true
		fi
	done
fi

# Append bat arguments.
if "$OPT_COLOR"; then
	BAT_ARGS+=("--color=always")
else
	BAT_ARGS+=("--color=never")
fi

# Initialize clear command based on whether or not ANSI should be used.
if [[ "$OPT_CLEAR" == "true" ]]; then
	if "$OPT_COLOR"; then
		clear() {
			term_clear || return $?
		}
	fi
else
	clear() {
		:
	}
fi

# -----------------------------------------------------------------------------
# Main:
# -----------------------------------------------------------------------------
if [[ "$OPT_MODE" = "file" ]]; then
	# Determine the watcher.
	if [[ -z "$OPT_WATCHER" ]]; then
		if ! determine_watcher; then
			print_error "Your system does not have any supported watchers."
			printc "Please read the documentation at %{BLUE}%s%{CLEAR} for more details.\n" "https://github.com/eth-p/bat-extras" 1>&2
			exit 2
		fi
	else
		if ! type "watcher_${OPT_WATCHER}_supported" &> /dev/null; then
			print_error "Unknown watcher: '%s'" "$OPT_WATCHER"
			exit 1
		fi
	
		if ! "watcher_${OPT_WATCHER}_supported" &> /dev/null; then
			print_error "Unsupported watcher: '%s'" "$OPT_WATCHER"
			exit 1
		fi
	fi
	
	main() {
		"watcher_${OPT_WATCHER}_watch"  "${FILES[@]}"
		return  $?
	}
else
	
	# Set bat's header to show the command.
	BAT_VERSION="$(bat_version)"
	if version_compare "$BAT_VERSION" -ge "0.14"; then
		BAT_ARGS+=(--file-name="${FILES[*]}")
	fi

	main() {
		local last_rendered
		local rendered
		local term_width="$(term_width)"
		BAT_ARGS+=("--terminal-width=$term_width")

		while true; do
			IFS='' rendered="$("${FILES[@]}" 2>&1 | "bat" "${BAT_ARGS[@]}")"
			if [ "$rendered" != "$last_rendered" ]; then
				# Only clear and redraw if there's a change.
				# This reduces excessive flickering.
				last_rendered="$rendered"
				clear
				printf "%s\n" "$rendered"
				rendered=''
			fi

			sleep "${OPT_INTERVAL}" || exit 1
		done
	}
fi

# Run the main function.
main
exit $?
