#!/usr/bin/env bash
# vim: filetype=bash :  -*- mode: sh; sh-shell: bash; -*-

set -euo pipefail

define() { IFS='\n' read -r -d '' ${1} || true ; }

myname="${0##*/}"

define pod <<"=cut"

=encoding utf-8

=head1 NAME

nup - N-up multi-column paged output for commands and files

=head1 SYNOPSIS

    nup [ options ] file ...
    nup -e [ options ] command ...

     -h  --help             show help
         --version          show version
     -d  --debug            debug mode
     -n  --dryrun           dry-run mode
     -e  --exec             execute command mode
         --alias=CMD=OPTS   set command alias
     -V  --parallel         parallel view mode
     -D  --document         document mode (default: on)
     -F  --fold             fold mode (disable page mode)
     -H  --filename         show filename headers (default: on)
     -G  --grid=#           grid layout (e.g., 2x3)
     -C  --pane=#           number of columns
     -R  --row=#            number of rows
     -P  --page=#           page height in lines
     -S  --pane-width=#     pane width (default: 85)
    --bs --border-style=#   border style (default: heavy-box)
    --ls --line-style=#     line style (none/truncate/wrap/wordwrap)
    --cm --colormap=#       color mapping (LABEL=COLOR)
         --pager=#          pager command (empty to disable)
         --no-pager         disable pager
         --white-board      black on white board
         --black-board      white on black board
         --green-board      white on green board
         --slate-board      white on dark slate board

=head1 VERSION

Version 0.9906

=cut

[[ $pod =~ Version\ +([0-9.]+) ]] && my_version=${BASH_REMATCH[1]}

##############################################################################
# Utility functions
##############################################################################

debug() {
    [[ ${debug:-} ]] && echo "debug: $*" >&2 || true
}

die() {
    echo "$myname: $*" >&2
    exit 1
}

# Check bash version (4.3+ required for getoptlong.sh)
if ((BASH_VERSINFO[0] < 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] < 3))); then
    die "bash 4.3+ required (found $BASH_VERSION)"
fi

##############################################################################
# Option definitions
##############################################################################

define USAGE <<END
nup - N-up multi-column paged output for commands and files

Usage: $myname [ options ] file ...
       $myname -e [ options ] command ...
END

declare -a up_opts=(--filename)  # up.pm options

# Default command aliases (command and options)
declare -A alias=(
    [bat]="bat --style=plain --color=always"
    [batcat]="batcat --style=plain --color=always"
    [rg]="rg --color=always"
    [tree]="tree -C"
)

declare -A OPTS=(
    [&REQUIRE]=0.6 [&USAGE]="$USAGE" [&PERMUTE]= [&DELIM]=
    [         help | h   !        # show help         ]=
    [      version |     !        # show version      ]=
    [        debug | d            # debug mode        ]=
    [       dryrun | n            # dry-run mode      ]=
    [         exec | e            # execute command   ]=
    [        alias | %            # command aliases   ]=
    [        pager |    :!        # pager command     ]=
    [     parallel | V   >up_opts # parallel view     ]=
    [     document | D   >up_opts # document mode     ]=
    [         fold | F   >up_opts # fold mode         ]=
    [     filename | H   >up_opts # show file headers ]=
    [         grid | G  :>up_opts # grid layout       ]=
    [         pane | C  :>up_opts # number of columns ]=
    [          row | R  :>up_opts # number of rows    ]=
    [         page | P  :>up_opts # page height       ]=
    [   pane-width | S  :>up_opts # pane width        ]=
    [ border-style | bs :>up_opts # border style      ]=
    [   line-style | ls :>up_opts # line style        ]=
    [     colormap | cm :>up_opts # color mapping     ]=
    [  white-board |     >up_opts # black on white    ]=
    [  black-board |     >up_opts # white on black    ]=
    [  green-board |     >up_opts # white on green    ]=
    [  slate-board |     >up_opts # white on slate    ]=
)

##############################################################################
# Option callbacks
##############################################################################

# -h callback: show help from POD
help() {
    sed -E \
        -e '/^$/N' \
        -e 's/^(\n*)=head[0-9]* */\1/' \
        -e '/^\n*[#=]/d' \
        -e '/Version/q' \
        <<< "$pod"
    exit 0
}

# --version callback: show version
version() {
    echo "$my_version"
    exit 0
}

# --pager callback: handle pager option
pager() {
    if [[ -z $2 ]]; then
        up_opts+=("--no-pager")
    else
        up_opts+=("--pager=$2")
    fi
}

##############################################################################
# Parse options
##############################################################################

. getoptlong.sh OPTS "$@"

##############################################################################
# Main
##############################################################################

# Show help if no arguments and stdin is a terminal
if (( $# == 0 )) && [[ -t 0 ]]; then
    help
fi

# Determine mode: command or file view
# Command mode if: -e specified, OR ($1 is not a file AND $1 is a command)
is_command_mode() {
    [[ ${exec:-} ]] && return 0
    [[ -f ${1:-} ]] && return 1
    command -v "${1:-}" >/dev/null 2>&1
}

# Expand command alias (command name + options)
expand_alias() {
    local cmd_name="${1##*/}"  # basename
    if [[ -v alias[$cmd_name] ]]; then
        echo "${alias[$cmd_name]}"
    else
        echo "$1"
    fi
}

if is_command_mode "$@"; then
    # Command mode: use optex -Mup
    local_cmd="$1"; shift
    declare -a expanded_cmd
    read -ra expanded_cmd <<< "$(expand_alias "$local_cmd")"
    declare -a cmd=(optex -Mup "${up_opts[@]}" -- "${expanded_cmd[@]}" "$@")
else
    # File view mode: use ansicolumn via optex -Mup
    declare -a cmd=(optex -Mup "${up_opts[@]}" -- ansicolumn "$@")
fi

debug "command: ${cmd[*]}"

[[ ${dryrun:-} ]] && cmd=(echo "${cmd[@]}")

exec "${cmd[@]}"

: <<'=cut'

=head1 DESCRIPTION

B<nup> is a simple wrapper script for C<optex -Mup>.  It provides a
convenient way to view files or run commands with N-up output
formatting using the L<App::optex::up> module.

=begin html

<p><img width="750" src="https://raw.githubusercontent.com/tecolicom/App-nup/main/images/nup.png"></p>

=end html

B<nup> automatically detects the mode based on the first argument:
if it is an existing file, file view mode is used; if it is an
executable command, command mode is used.  Use C<-e> option to
force command mode when needed.

=head1 OPTIONS

=head2 General Options

=over 4

=item B<-h>, B<--help>

Show help message.

=item B<--version>

Show version.

=item B<-d>, B<--debug>

Enable debug mode.

=item B<-n>, B<--dryrun>

Dry-run mode. Show the command without executing.

=item B<-e>, B<--exec>

Force command execution mode. Normally the mode is auto-detected,
but use this option when you want to execute a file as a command.

=item B<--alias>=I<NAME>=I<CMD> I<OPTS>...

Define command alias. When a command matches I<NAME>, it is replaced
by I<CMD> with specified I<OPTS>.  This can be used to add default
options or to substitute a different command.
Multiple C<--alias> options can be specified.

Default aliases:

    bat     bat --style=plain --color=always
    batcat  batcat --style=plain --color=always
    rg      rg --color=always
    tree    tree -C

Example:

    nup --alias='grep=ggrep --color=always' grep pattern file

=item B<-V>, B<--parallel>

Enable parallel view mode for ansicolumn.  In this mode, each file
is displayed in its own column without pagination, similar to
C<--fold>.  Automatically enabled when multiple files are
specified.  Single file or stdin input results in single column
output.

=item B<-D>, B<--document>

Enable document mode for ansicolumn.  This mode is optimized for
viewing documents with page-based layout.  Enabled by default.
Use C<--no-document> to disable.

=item B<-F>, B<--fold>

Enable fold mode (disable page mode).  In fold mode, the entire
content is split evenly across columns without pagination.  Page
mode is the default.

=item B<-H>, B<--filename>

Show filename headers in file view mode. Enabled by default.
Use C<--no-filename> to disable.

=back

=head2 Layout Options

=over 4

=item B<-C> I<N>, B<--pane>=I<N>

Set the number of columns (panes).

=item B<-R> I<N>, B<--row>=I<N>

Set the number of rows.

=item B<-G> I<CxR>, B<--grid>=I<CxR>

Set grid layout. For example, C<-G2x3> creates 2 columns and 3 rows.

=item B<-P> I<N>, B<--page>=I<N>

Set the page height in lines.

=item B<-S> I<N>, B<--pane-width>=I<N>

Set the pane width in characters. Default is 85.

=back

=head2 Style Options

=over 4

=item B<--bs>=I<STYLE>, B<--border-style>=I<STYLE>

Set the border style. Default is C<heavy-box>.

=item B<--ls>=I<STYLE>, B<--line-style>=I<STYLE>

Set the line style. Available: C<none>, C<truncate>, C<wrap>, C<wordwrap>.

=item B<--cm>=I<SPEC>, B<--colormap>=I<SPEC>

Set color mapping. Specify as C<LABEL=COLOR> (e.g., C<--cm=BORDER=R>).
Available labels: C<TEXT>, C<BORDER>.

=item B<--white-board>, B<--black-board>, B<--green-board>, B<--slate-board>

Predefined color schemes for board-style display.

=back

=head2 Pager Options

=over 4

=item B<--pager>=I<COMMAND>

Set the pager command. Default is C<NUP_PAGER> or C<less -F +Gg>.
The C<PAGER> variable is not used to avoid an infinite loop when
C<PAGER> is set to C<nup>.
Use C<--pager=> (empty) or C<--no-pager> to disable pager.

=item B<--no-pager>

Disable pager.

=back

=head1 EXAMPLES

    nup man nup                # view manual in multi-column
    nup -C2 man perl           # 2 columns
    nup -G2x2 man perl         # 2x2 grid (4-up)
    nup -F man perl            # fold mode (no pagination)
    nup file1.txt file2.txt    # view files side by side
    nup -e ./script.sh         # force command mode for a file

=head1 INSTALLATION

Using L<cpanminus|https://metacpan.org/pod/App::cpanminus>:

    cpanm -n App::nup

=head1 DIAGNOSTICS

Both stdout and stderr of the command are merged and passed through
the output filter.  Error messages will appear in the paged output.

=head1 EXIT STATUS

The exit status of the executed command is not preserved because
the output is passed through a filter pipeline.

=head1 SEE ALSO

L<App::optex::up>, L<optex>

=head1 AUTHOR

Kazumasa Utashiro

=head1 LICENSE

Copyright 2025 Kazumasa Utashiro.

This software is released under the MIT License.
L<https://opensource.org/licenses/MIT>

=cut