#!/bin/bash
# -----------------------------------------------------------------------------
# bat-extras | Copyright (C) 2021 eth-p | MIT License
#
# Repository: https://github.com/eth-p/bat-extras
# Issues:     https://github.com/eth-p/bat-extras/issues
# -----------------------------------------------------------------------------
#
# EXTERNAL VIEWERS FOR BATPIPE:
#
#     External viewers can be added to batpipe by creating bash scripts
#     inside the `~/.config/batpipe/viewers.d/` directory.
#
# CREATING A VIEWER:
#
#      Viewers must define two functions and append the viewer's name to the
#      `BATPIPE_VIEWERS` array.
#
#      - viewer_${viewer}_supports [file_basename] [file_path] [inner_file_path]
#        If this returns 0, the viewer's process function will be used.
#
#      - viewer_${viewer}_process  [file_path] [inner_file_path]
#
# VIEWER API:
#
#     $BATPIPE_VIEWERS      -- An array of loaded file viewers.
#     $BATPIPE_ENABLE_COLOR -- Whether color is supported. (`true`|`false`)
#     $BATPIPE_INSIDE_LESS  -- Whether batpipe is inside less. (`true`|`false`)
#     $TERM_WIDTH           -- The terminal width. (only supported in `less`)
#
#     batpipe_header [pattern] [...]    -- Print a viewer header line.
#     batpipe_subheader [pattern] [...] -- Print a viewer subheader line.
#
#     bat                   -- Use `bat` for highlighting. If running inside `bat`, does nothing.
#
#     strip_trailing_slashes [path]     -- Strips trailing slashes from a path.
#
# -----------------------------------------------------------------------------
# shellcheck disable=SC1090 disable=SC2155
SELF="$(cd "$(dirname "${BASH_SOURCE[0]}")" && cd "$(dirname "$(readlink "${BASH_SOURCE[0]}" || echo ".")")" && pwd)/$(basename "${BASH_SOURCE[0]}")"
# --- BEGIN LIBRARY FILE: dirs.sh ---

# Gets a configuration directory for a command-line program.
# Arguments:
#    1   -- The program name.
config_dir() {
	if [[ -n "${XDG_CONFIG_HOME+x}" ]]; then
		echo "${XDG_CONFIG_HOME}/$1"
	else
		echo "${HOME}/.config/$1"
	fi
}
# --- END LIBRARY FILE ---
# --- BEGIN LIBRARY FILE: str.sh ---

# Converts a string to lower case.
tolower() {
	tr "[:upper:]" "[:lower:]" <<<"$1"
}

# Converts a string to upper case.
toupper() {
	tr "[:lower:]" "[:upper:]" <<<"$1"
}
# --- END LIBRARY FILE ---
# --- BEGIN LIBRARY FILE: print.sh ---

# Printf, but with optional colors.
# This uses the same syntax and arguments as printf.
#
# Example:
#     printc "%{RED}This is red %s.%{CLEAR}\n" "text"
#
printc() {
	printf "$(sed "$_PRINTC_PATTERN" <<<"$1")" "${@:2}"
}

# Initializes the color tags for printc.
#
# Arguments:
#     true  -- Turns on color output.
#     false -- Turns off color output.
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 a warning message to stderr.
# Arguments:
#     1   -- The printc formatting string.
#     ... -- The printc formatting arguments.
print_warning() {
	printc "%{YELLOW}[%s warning]%{CLEAR}: $1%{CLEAR}\n" "batpipe" "${@:2}" 1>&2
}

# Print an error message to stderr.
# Arguments:
#     1   -- The printc formatting string.
#     ... -- The printc formatting arguments.
print_error() {
	printc "%{RED}[%s error]%{CLEAR}: $1%{CLEAR}\n" "batpipe" "${@:2}" 1>&2
}

# Initialization:
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: path.sh ---

# Gets the file extension from a file path.
# Arguments:
#     1   -- The file path.
extname() {
	local file="$1"
	echo ".${file##*.}"
}

# Strips trailing slashes from a file path.
# Arguments:
#     1   -- The file path.
strip_trailing_slashes() {
	local file="$1"
	while [[ -n "$file" && "${file: -1}" = "/" ]]; do
		file="${file:0:$((${#file}-1))}"
	done
	echo "$file"
}
# --- END LIBRARY FILE ---
# --- BEGIN LIBRARY FILE: proc.sh ---

# Gets the path to the parent executable file.
# Arguments:
#     1   -- The target pid. If not provided, the script's parent is used.
parent_executable() {
	local target_pid="${1:-$PPID}"
	ps -f -p "$target_pid" | tail -n 1 | awk '{for(i=8;i<=NF;i++) printf $i" "; printf "\n"}'
}

# Gets the PID of the parent executable file.
# Arguments:
#     1   -- The target pid. If not provided, the script's parent is used.
parent_executable_pid() {
	local target_pid="${1:-$PPID}"
	ps -f -p "$target_pid" | tail -n 1 | awk '{print $3}'
}

# Gets the path to the parent login shell.
# Arguments:
#     1   -- The target pid. If not provided, the script's parent is used.
parent_shell() {
	local target_pid="${1:-$PPID}"
	local target_name
	while true; do
		{
			read -r target_pid
			read -r target_name
			
			# If the parent process starts with a "-", it's a login shell.
			if [[ "${target_name:0:1}" = "-" ]]; then
				target_name="$(cut -f1 -d' ' <<< "${target_name:1}")"
				break
			fi
			
			# If the parent process has "*sh " followed by "-l", it's probably a login shell.
			if [[ "$target_name" =~ sh\ .*-l ]]; then
				target_name="$(cut -f1 -d' ' <<< "${target_name}")"
				break
			fi
			
			# If the parent process is pid 0 (init), then we haven't found a parent shell.
			# At this point, it's best to assume the shell is whatever is defined in $SHELL.
			if [[ "$target_pid" -eq 0 ]]; then
				target_name="$SHELL"
				break
			fi
		} < <({
			ps -f -p "$target_pid" \
				| tail -n 1 \
				| awk '{print $3; for(i=8;i<=NF;i++) printf $i" "; printf "\n"}'
		})
	done
	
	# Ensure that the detected shell is an executable path.
	if [[ -f "$target_name" ]]; then
		echo "$target_name"
	elif ! command -v "$target_name" 2>/dev/null; then
		echo "$target_name" # It's not, but we have nothing else we can do here.
	fi
}
# --- END LIBRARY FILE ---
# --- BEGIN LIBRARY FILE: opt.sh ---

# An array of functions to call before returning from `shiftopt`.
#
# If one of these functions returns a successful exit code, the
# option will be transparently skipped instead of handled.
SHIFTOPT_HOOKS=()

# A setting to change how `shiftopt` will interpret short options that consist
# of more than one character.
#
# Values:
#
#     SPLIT  -- Splits the option into multiple single-character short options.
#               "-abc" -> ("-a" "-b" "-c")
#     
#     VALUE  -- Uses the remaining characters as the value for the short option.
#               "-abc" -> ("-a=bc")
#
#     CONV   -- Converts the argument to a long option.
#               "-abc" -> ("--abc")
#
#     PASS   -- Pass the argument along as-is.
#               "-abc" -> ("-abc")
#
SHIFTOPT_SHORT_OPTIONS="VALUE"

# Sets the internal _ARGV, _ARGV_INDEX, and _ARGV_LAST variables used when
# parsing options with the shiftopt and shiftval functions.
#
# Arguments:
#     ... -- The program arguments.
# 
# Example:
#     setargs "--long=3" "file.txt"
setargs() {
	_ARGV=("$@")
	_ARGV_LAST="$((${#_ARGV[@]} - 1))"
	_ARGV_INDEX=0
	_ARGV_SUBINDEX=1
}

# Gets all the remaining unparsed arguments and saves them to a variable.
#
# Arguments:
#     "-a" -- Append the arguments to the variable instead of replacing it.
#     $1   -- The variable to save the args to.
# 
# Example:
#     getargs remaining_args
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
}

# Resets the internal _ARGV* variables to the original script arguments.
# This is the equivalent of storing the top-level $@ and using setargs with it.
resetargs() {
	setargs "${_ARGV_ORIGINAL[@]}"
}

# INTERNAL.
#
# Increments the argv index pointer used by `shiftopt`.  
_shiftopt_next() {
	_ARGV_SUBINDEX=1
	((_ARGV_INDEX++)) || true
}

# Gets the next option passed to the script.
#
# Variables:
#     OPT  -- The option name.
#
# Returns:
#     0  -- An option was read.
#     1  -- No more options were read.
#
# Example:
#     while shiftopt; do
#         shiftval
#         echo "$OPT = $OPT_VAL"
#     done
shiftopt() {
	# Read the top of _ARGV.
	[[ "$_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
	
	# Handle short options.
	if [[ "$OPT" =~ ^-[^-]{2,} ]]; then
		case "$SHIFTOPT_SHORT_OPTIONS" in
		 	# PASS mode: "-abc=0" -> ("-abc=0")
			PASS) _shiftopt_next ;;

			# CONV mode: "-abc=0" -> ("--abc=0")
			CONV) OPT="-${OPT}"; _shiftopt_next ;; 

			# VALUE mode: "-abc=0" -> ("-a=bc=0")
			VALUE) {
				OPT="${_ARGV[$_ARGV_INDEX]}"
				OPT_VAL="${OPT:2}"
				OPT="${OPT:0:2}"
				_shiftopt_next
			} ;; 

			# SPLIT mode: "-abc=0" -> ("-a=0" "-b=0" "-c=0")
			SPLIT) {
				OPT="-${OPT:$_ARGV_SUBINDEX:1}"
				((_ARGV_SUBINDEX++)) || true
				if [[ "$_ARGV_SUBINDEX" -gt "${#OPT}" ]]; then
					_shiftopt_next
				fi
			} ;;

			# ????? mode: Treat it as pass.
			*)
				printf "shiftopt: unknown SHIFTOPT_SHORT_OPTIONS mode '%s'" \
					"$SHIFTOPT_SHORT_OPTIONS" 1>&2
				_shiftopt_next
				;;
		esac
	else
		_shiftopt_next
	fi

	# Handle hooks.
	local hook
	for hook in "${SHIFTOPT_HOOKS[@]}"; do
		if "$hook"; then
			shiftopt
			return $?
		fi
	done

	return 0
}

# Gets the value for the current option.
#
# Variables:
#     OPT_VAL  -- The option value.
#
# Returns:
#     0       -- An option value was read.
#     EXIT 1  -- No option value was available.
shiftval() {
	# Skip if a value was already provided.
	if [[ -n "${OPT_VAL+x}" ]]; then
		return 0
	fi
	
	if [[ "$_ARGV_SUBINDEX" -gt 1 && "$SHIFTOPT_SHORT_OPTIONS" = "SPLIT" ]]; then
		# If it's a short group argument in SPLIT mode, we grab the next argument.
		OPT_VAL="${_ARGV[$((_ARGV_INDEX+1))]}"
	else
		# Otherwise, we can handle it normally.
		OPT_VAL="${_ARGV[$_ARGV_INDEX]}"
		_shiftopt_next
	fi

	# Error if no value is provided.
	if [[ "$OPT_VAL" =~ -.* ]]; then
		printc "%{RED}%s: '%s' requires a value%{CLEAR}\n" "batpipe" "$ARG"
		exit 1
	fi
}

setargs "$@"
_ARGV_ORIGINAL=("$@")
# --- END LIBRARY FILE ---
# --- BEGIN LIBRARY FILE: version.sh ---

# Gets the current bat version.
bat_version() {
	if [[ -z "${__BAT_VERSION}" ]]; then
		__BAT_VERSION="$(command "bat" --version | cut -d ' ' -f 2)"
	fi
	
	echo "${__BAT_VERSION}"
}

# Compares two version strings.
# Arguments:
#    1  -- The version to compare.
#    2  -- The comparison operator (same as []).
#    3  -- The version to compare with.
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"

	# Extract the leading number.
	local v_major="${version%%.*}"
	local c_major="${compare%%.*}"

	# Extract the remaining numbers.
	local v_minor="${version#*.}"
	local c_minor="${compare#*.}"

	# Compare the versions specially if the final number has been reached.
	if [[ -z "$v_minor" && -z "$c_minor" ]]; then
		[ "$v_major" $operator "$c_major" ];
		return $?
	fi

	# Insert zeroes where there are missing numbers.
	if [[ -z "$v_minor" ]]; then
		v_minor="0."
	fi

	if [[ -z "$c_minor" ]]; then
		c_minor="0."
	fi

	# Compare the versions.
	# This is an early escape case.
	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 ---
# --- BEGIN LIBRARY FILE: term.sh ---

# Gets the width of the terminal.
# This will return 80 unless stdin is attached to the terminal.
#
# Returns:
#     The terminal width, or 80 if there's no TTY.
#
term_width() {
	# shellcheck disable=SC2155
	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
}

# Clears the terminal using the ANSI escape sequences for erase screen and cursor absolute positioning.
term_clear() {
	printf "\x1B[3J\x1B[2J\x1B[H"
}
# --- END LIBRARY FILE ---
# -----------------------------------------------------------------------------
# Usage/Install:
# -----------------------------------------------------------------------------

if [[ "$#" -eq 0 ]]; then
	# If writing to a terminal, display instructions and help.
	if [[ -t 1 ]]; then
		printc "%{DIM}# %s, %s.\n# %s\n# %s\n# %s\n# \n# %s%{CLEAR}\n" \
			"batpipe" \
			"a bat-based preprocessor for less and bat" \
			"Version: 2022.07.27" \
			"Homepage: https://github.com/eth-p/bat-extras" \
			"Copyright (C) 2019-2021 eth-p | MIT License" \
			"To use batpipe, eval the output of this command in your shell init script."
	fi

	# Detect the shell.
	#
	# This will directly check if the parent is fish, since there's a
	# good chance that `bash` or `sh` will be invoking fish.
	if [[ "$(basename -- "$(parent_executable | cut -f1 -d' ')")" == "fish" ]]; then
		detected_shell="fish"
	else
		detected_shell="$(parent_shell)"
	fi

	# Print the commands required to add `batpipe` to the environment variables.
	case "$(basename -- "${detected_shell:bash}")" in
		fish) # Fish
			printc '%{YELLOW}set -x %{CLEAR}LESSOPEN %{CYAN}"|%q %%s"%{CLEAR};\n' "$SELF"
			printc '%{YELLOW}set -e %{CLEAR}LESSCLOSE;\n'
			;;
		*) # Bash-like
			printc '%{MAGENTA}LESSOPEN%{CLEAR}=%{CYAN}"|%s %%s"%{CLEAR};\n' "$SELF"
			printc '%{YELLOW}export%{CLEAR} LESSOPEN;\n' "$SELF"
			printc '%{YELLOW}unset%{CLEAR} LESSCLOSE;\n'
			;;
	esac
	
	# Print the commands required to use color in `less` with `batpipe`.
	if [[ -t 1 ]]; then
		printc "\n%{DIM}# The following will enable colors when using batpipe with less:%{CLEAR}\n"
	fi
	
	# shellcheck disable=SC2016
	case "$(basename -- "${detected_shell:bash}")" in
		fish) # Fish
			printc '%{YELLOW}set -x %{CLEAR}LESS %{CYAN}"%{MAGENTA}$LESS%{CYAN} -R"%{CLEAR};\n' "$SELF"
			printc '%{YELLOW}set -x %{CLEAR}BATPIPE %{CYAN}"color"%{CLEAR};\n'
			;;
		*) # Bash-like
			printc '%{MAGENTA}LESS%{CLEAR}=%{CYAN}"%{MAGENTA}$LESS%{CYAN} -R"%{CLEAR};\n' "$SELF"
			printc '%{MAGENTA}BATPIPE%{CLEAR}=%{CYAN}"color"%{CLEAR};\n' "$SELF"
			printc '%{YELLOW}export%{CLEAR} LESS;\n' "$SELF"
			printc '%{YELLOW}export%{CLEAR} BATPIPE;\n' "$SELF"
			;;
	esac
	exit 0
fi

# -----------------------------------------------------------------------------
# Init:
# -----------------------------------------------------------------------------
BATPIPE_INSIDE_LESS=false
BATPIPE_INSIDE_BAT=false
TERM_WIDTH="$(term_width)"

if [[ "$(basename -- "$(parent_executable | cut -f1 -d' ')")" == less ]]; then
	BATPIPE_INSIDE_LESS=true
elif [[ "$(basename -- "$(parent_executable | cut -f1 -d' ')")" == "$(basename -- "bat")" ]]; then
	BATPIPE_INSIDE_BAT=true
fi

# -----------------------------------------------------------------------------
# Viewers:
# -----------------------------------------------------------------------------

BATPIPE_VIEWERS=("exa" "ls" "tar" "unzip" "gunzip" "xz")

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

viewer_exa_supports() {
	[[ -d "$2" ]] || return 1
	command -v "exa" &> /dev/null || return 1
	return 0
}

viewer_exa_process() {
	local dir="$(strip_trailing_slashes "$1")"
	batpipe_header "Viewing contents of directory: %{PATH}%s" "$dir"
	if "$BATPIPE_ENABLE_COLOR"; then
		exa -la --color=always "$1" 2>&1
	else
		exa -la --color=never "$1" 2>&1
	fi
	return $?
}

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

viewer_ls_supports() {
	[[ -d "$2" ]]
	return $?
}

viewer_ls_process() {
	local dir="$(strip_trailing_slashes "$1")"
	batpipe_header "Viewing contents of directory: %{PATH}%s" "$dir"
	ls -lA "$1" 2>&1
	return $?
}

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

viewer_tar_supports() {
	command -v "tar" &> /dev/null || return 1

	case "$1" in
		*.tar | *.tar.*) return 0 ;;
	esac

	return 1
}

viewer_tar_process() {
	if [[ -n "$2" ]]; then
		tar -xf "$1" -O "$2" | bat --file-name="$1/$2"
	else
		batpipe_header    "Viewing contents of archive: %{PATH}%s" "$1"
		batpipe_subheader "To view files within the archive, add the file path after the archive."
		tar -tvf "$1"
		return $?
	fi
}

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

viewer_unzip_supports() {
	command -v "unzip" &> /dev/null || return 1

	case "$1" in
		*.zip) return 0 ;;
	esac

	return 1
}

viewer_unzip_process() {
	if [[ -n "$2" ]]; then
		unzip -p "$1" "$2" | bat --file-name="$1/$2"
	else
		batpipe_header    "Viewing contents of archive: %{PATH}%s" "$1"
		batpipe_subheader "To view files within the archive, add the file path after the archive."
		unzip -l "$1"
		return $?
	fi
}

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

viewer_gunzip_supports() {
	command -v "gunzip" &> /dev/null || return 1
	[[ -z "$3" ]] || return 1

	case "$2" in
		*.gz) return 0 ;;
	esac

	return 1
}

viewer_gunzip_process() {
	gunzip -k -c "$1" | bat --file-name="$1"
	return $?
}

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

viewer_xz_supports() {
	command -v "xz" &> /dev/null || return 1
	[[ -z "$3" ]] || return 1

	case "$2" in
		*.xz) return 0 ;;
	esac

	return 1
}

viewer_xz_process() {
	xz --decompress -k -c "$1" | bat --file-name="$1"
	return $?
}

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

# Print a header for batpipe messages.
# Arguments:
#     1   -- The printc formatting string.
#     ... -- The printc formatting arguments.
batpipe_header() {
	local pattern="${1//%{C\}/%{C\}%{HEADER\}}"
	printc "%{HEADER}==> $pattern%{C}\n" "${@:2}"
}

# Print a subheader for batpipe messages.
# Arguments:
#     1   -- The printc formatting string.
#     ... -- The printc formatting arguments.
batpipe_subheader() {
	local pattern="${1//%{C\}/%{C\}%{SUBHEADER\}}"
	printc "%{SUBHEADER}==> $pattern%{C}\n" "${@:2}"
}

# Executes `bat` (or `cat`, if already running from within `bat`).
# Supports the `--file-name` argument if the bat version is new enough.
#
# NOTE: The `--key=value` option syntax is required for full compatibility.
bat() {
	# Conditionally enable forwarding of certain arguments.
	if [[ -z "$__BAT_VERSION" ]]; then
		__BAT_VERSION="$(bat_version)"

		__bat_forward_arg_file_name() { :; }

		if version_compare "$__BAT_VERSION" -ge "0.14"; then
			__bat_forward_arg_file_name() {
				__bat_forward_args+=("--file-name" "$1")
			}
		fi
	fi

	# Parse arguments intended for bat.
	__bat_batpipe_args=()
	__bat_forward_args=()
	__bat_files=()
	setargs "$@"
	while shiftopt; do
		case "$OPT" in
			--file-name)
				shiftval
				__bat_forward_arg_file_name                       "$OPT_VAL"
				;;

			# Disallowed forwarding.
			--paging)            shiftval ;;
			--decorations)       shiftval ;;
			--style)             shiftval ;;
			--terminal-width)    shiftval ;;
			--plain | -p | -pp | -ppp) : ;;

			# Forward remaining.
			-*) {
				__bat_forward_args+=("$OPT")
				if [[ -n "$OPT_VAL" ]]; then
					__bat_forward_args+=("$OPT_VAL")
				fi
			} ;;

			*) {
				__bat_forward_args+=("$OPT")
				__bat_files=("$OPT")
			} ;;
		esac
	done

	# Insert batpipe arguments.
	if "$BATPIPE_INSIDE_LESS"; then
		__bat_batpipe_args+=(--decorations=always)
		__bat_batpipe_args+=(--terminal-width="$TERM_WIDTH")
		if "$BATPIPE_ENABLE_COLOR"; then
			__bat_batpipe_args+=(--color=always)
		else
			__bat_batpipe_args+=(--color=never)
		fi
	fi

	if "$BATPIPE_INSIDE_BAT"; then
		if [[ "${#__bat_files[@]}" -eq 0 ]]; then
			cat 
		else
			cat "${__bat_files[@]}"
		fi
	fi

	# Execute the real bat.
	command "bat" --paging=never "${__bat_batpipe_args[@]}" "${__bat_forward_args[@]}"
}

# -----------------------------------------------------------------------------
# Colors:
# -----------------------------------------------------------------------------

printc_init "[DEFINE]" << END
	C			\x1B[0m
	SUBPATH		\x1B[2;35m
	PATH		\x1B[0;35m
	HEADER		\x1B[0;36m
	SUBHEADER	\x1B[2;36m
END

# Enable color output if:
# - Parent is not less OR BATPIPE=color; AND
# - NO_COLOR is not defined.
#
# shellcheck disable=SC2034
if [[ "$BATPIPE_INSIDE_LESS" == "false" || "$BATPIPE" == "color" ]] && [[ -z "${NO_COLOR+x}" ]]; then
	BATPIPE_ENABLE_COLOR=true
	printc_init true
else
	BATPIPE_ENABLE_COLOR=false
	printc_init false
fi

# -----------------------------------------------------------------------------
# Main:
# -----------------------------------------------------------------------------

__CONFIG_DIR="$(config_dir batpipe)"
__TARGET_INSIDE=""
__TARGET_FILE="$(strip_trailing_slashes "$1")"

# Determine the target file by walking upwards from the specified path.
# This allows inner paths of archives to be used.
while ! [[ -e "$__TARGET_FILE" ]]; do
	__TARGET_INSIDE="$(basename -- "${__TARGET_FILE}")/${__TARGET_INSIDE}"
	__TARGET_FILE="$(dirname -- "${__TARGET_FILE}")"
done

# If the target file isn't actually a file, then the inner path should be appended.
if ! [[ -f "$__TARGET_FILE" ]]; then
	__TARGET_FILE="${__TARGET_FILE}/${__TARGET_INSIDE}"
	__TARGET_INSIDE=""
fi

# If an inner path exists or the target file isn't a directory, the target file should not have trailing slashes.
if [[ -n "$__TARGET_INSIDE" ]] || ! [[ -d "$__TARGET_FILE" ]]; then
	__TARGET_FILE="$(strip_trailing_slashes "$__TARGET_FILE")"
fi

# Remove trailing slash of the inner target path.
__TARGET_INSIDE="$(strip_trailing_slashes "$__TARGET_INSIDE")"
__TARGET_BASENAME="$(basename -- "$__TARGET_FILE")"

# Stop bat from calling this recursively.
unset LESSOPEN
unset LESSCLOSE

# Load external viewers.
if [[ -d "${__CONFIG_DIR}/viewers.d" ]]; then
	unset LIB
	unset SELF

	shopt -s nullglob
	for viewer_script in "${__CONFIG_DIR}/viewers.d"/*; do
		source "${viewer_script}"
	done
	shopt -u nullglob
fi

# Try opening the file with the first viewer that supports it.
for viewer in "${BATPIPE_VIEWERS[@]}"; do
	if "viewer_${viewer}_supports" "$__TARGET_BASENAME" "$__TARGET_FILE" "$__TARGET_INSIDE" 1>&2; then
		"viewer_${viewer}_process" "$__TARGET_FILE" "$__TARGET_INSIDE"
		exit $?
	fi
done

# No supported viewer. Just pass it through (if using bat).
if [[ "$BATPIPE_INSIDE_BAT" == true ]]; then
	exit 1
fi

# No supported viewer... highlight it using bat.
if [[ -f "$1" ]]; then
	bat "$1"
fi
