You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

441 lines
11 KiB
Bash

#!/usr/bin/env -S bash -e
#
# Copyright 2021 Nathan L. Conrad
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
# Establish usage
IFS= read -rd '' usage << '__eof__' || true
Usage: glitz [-h] [...] [-] [...]
Wrap shell invocation with styled status indications
Optional arguments:
-h, --help Show this help message and exit
-, -- Explicitly denote the end of optional arguments. Required if a
positional argument is supplied after any option begining with
'--'. Without insight into which long options expect parameters,
this differentiates the first positional argument from the
parameter of an option.
Arguments other than -h/--help are passed to the shell invocation, except for
the -i option, which is not supported. If the first positional parameter is
'---' and either the -c option is set or the -s option is not set, a command or
file is not executed. Instead, a sylized horizontal divider is written to
standard output.
Environment variables:
GLITZ_GLYPHS A string which specifies the character set used for
stylization. Valid values are '437' and 'nerd-font'.
Defaults to 7-bit ASCII if unset, null, or invalid.
GLITZ_SHELL The shell to invoke. Defaults to $SHELL if unset or null,
'/bin/sh' if SHELL is unset or null.
__eof__
# Usage: argerr msg
#
# Write an argument usage error message to standard error and fail
#
# Positional arguments:
# msg The core error message
function argerr
{
echo "$1. Use -h for help." >&2
exit 1
}
# Parse optional arguments
arg_index=0
cmd=0
stdin=0
long_opt=0
unset opts
while (( ++arg_index <= $# ))
do
case ${!arg_index} in
-|--)
(( ++arg_index ))
break
;;
---)
(( long_opt )) || break
;;
--help)
echo -n "$usage"
exit 0
;;
--*)
long_opt=1
;;
-*h*)
echo -n "$usage"
exit 0
;;
-*i*)
argerr '-i is not supported'
;;
-*c*)
cmd=1
stdin=0
;;
-*s*)
(( cmd )) || stdin=1
;;
-*)
;;
*)
(( long_opt )) || break
esac
opts+=("${!arg_index}")
done
# Usage: try [...]
#
# Execute arguments as a command while suppressing errors
function try
{
local errexit stdout err
[[ -o errexit ]] && errexit=1 || errexit=0
(( errexit )) && set +e
stdout=$("$@" 2> /dev/null)
err=$?
(( errexit )) && set -e
(( err )) || printf '%s\n' "$stdout"
}
# Cache the escape sequences of terminal attributes
color_names=(black red green yellow blue magenta cyan white grey)
normal=$(try tput sgr0)
unset bold colors bgnd fgnd
monochrome=1
for i in $(seq 0 $(( ${#color_names[@]} - 1 )))
do
eval "${color_names[$i]}=$i"
done
if [[ -n $normal ]]
then
bold=$(try tput bold)
colors=$(try tput colors)
monochrome=$(( colors < 256 ))
if (( ! monochrome ))
then
for i in $(seq 0 $(( ${#color_names[@]} - 1 )))
do
bgnd[$i]=$(try tput setab $i)
fgnd[$i]=$(try tput setaf $i)
done
fi
fi
# Cache glyphs
charmap=$(try locale charmap)
[[ $charmap = UTF-8 ]] && char_set=${GLITZ_GLYPHS:-ascii} || char_set=ascii
unset vbar ledge redge imargin omargin stime file tty xfer success failure \
timer positions
case $char_set in
437)
extends=$'\xc2\xbb'
hbar=$'\xe2\x94\x80'
(( monochrome )) || vbar=$'\xe2\x94\x82'
(( monochrome )) && ledge=$bold[
(( monochrome )) && redge=$bold]
imargin=' '
(( monochrome )) && omargin=' '
for i in $(seq 0 9)
do
positions[$i]="$i "
done
;;
nerd-font)
extends=$'\xef\x98\xbd'
hbar=$'\xe2\x94\x80'
(( monochrome )) && ledge=$'\xee\x82\xb7' || ledge=$'\xee\x82\xb6'
(( monochrome )) && redge=$'\xee\x82\xb5' || redge=$'\xee\x82\xb4'
(( monochrome )) && imargin=' '
(( monochrome )) || omargin=' '
stime=$'\xef\x99\x94 '
file=$'\xef\x83\xb6 '
tty=$'\xef\x9a\x8c '
xfer=$'\xef\x92\x92 '
success=$'\xef\x89\x9b '
failure=$'\xee\x88\xb1 '
timer=$'\xef\x99\x90 '
positions[0]=$'\xef\xa2\xa1 '
positions[1]=$'\xef\xa2\xa4 '
positions[2]=$'\xef\xa2\xa7 '
positions[3]=$'\xef\xa2\xaa '
positions[4]=$'\xef\xa2\xad '
positions[5]=$'\xef\xa2\xb0 '
positions[6]=$'\xef\xa2\xb3 '
positions[7]=$'\xef\xa2\xb6 '
positions[8]=$'\xef\xa2\xb9 '
positions[9]=$'\xef\xa2\xbc '
;;
*)
extends=$'...'
hbar=-
(( monochrome )) || vbar='|'
(( monochrome )) && ledge=$bold[
(( monochrome )) && redge=$bold]
imargin=' '
(( monochrome )) && omargin=' '
for i in $(seq 0 9)
do
positions[$i]="$i "
done
esac
# Usage: twidth
#
# Output the terminal width
function twidth
{
local cols
cols=$(try tput cols)
[[ -n $cols ]] || cols=80
echo "$cols"
}
# Usage: hdiv
#
# Output a horizontal divider and exit
function hdiv
{
echo -n "$normal${fgnd[$black]}"
printf "%.0s$hbar" $(seq $(twidth))
echo "$normal"
exit 0
}
# Usage: sanitize str
#
# Output a string with ugly whitespace and control codes removed
#
# Positional arguments:
# str A string
function sanitize
{
set -- "${1//$'\b'}"
set -- "${1//$'\f'}"
set -- "${1//$'\n'/' '}"
set -- "${1//$'\r'}"
set -- "${1//$'\t'/' '}"
set -- "${1//$'\v'/' '}"
printf '%s\n' "$1"
}
# Usage: dlen str
#
# Output the display length of a string
#
# Positional arguments:
# str A string
function dlen
{
set -- "${1//$normal}"
set -- "${1//$bold}"
for i in "${bgnd[@]}"
do
set -- "${1//$i}"
done
for i in "${fgnd[@]}"
do
set -- "${1//$i}"
done
printf '%s\n' "$1" | wc -L
}
# Usage: trunc str len
#
# Output a length-limited string
#
# Positional arguments:
# str A string
# len The maximum display length
function trunc
{
local len fmt
len=$(dlen "$1")
if (( len > $2 ))
then
local extends_len
extends_len=$(dlen "$extends")
if (( $2 >= extends_len ))
then
while true
do
set -- "${1%?}" "$2"
len=$(dlen "$1")
(( len > $2 - extends_len )) || break
done
fmt=%s
set -- "$1$extends"
fi
else
fmt=%s
fi
printf "$fmt\n" "$1"
}
# Usage: field str [bgnd] [fgnd]
#
# Output a stylized text field
#
# Positional arguments:
# str The field text
# bgnd The background color index
# fgnd The foreground color index
function field
{
local edge backgnd foregnd
backgnd=$2
if [[ -n $backgnd ]]
then
edge=$normal${fgnd[$backgnd]}
backgnd=${bgnd[$backgnd]}
fi
foregnd=$3
[[ -n $foregnd ]] && foregnd=${fgnd[$foregnd]}
printf %s "$edge$ledge$normal$backgnd$foregnd$imargin$1" \
"$normal$backgnd$foregnd$imargin$edge$redge$normal"
echo
}
# Default the shell
shell=${GLITZ_SHELL:-${SHELL:-/bin/sh}}
shell_name=$(basename -- "$shell")
# Use options and the first positional argument to infer the execution source.
# If the command string or filename argument is '---', output a horizontal
# divider and exit.
if (( cmd ))
then
(( arg_index > $# )) && argerr 'Missing command string argument'
arg=${!arg_index}
[[ $arg = --- ]] && hdiv
[[ -n $arg ]] || arg=:
(( ++arg_index ))
if [[ -n $SOCKHOP_SOCK ]]
then
(( arg_index <= $# )) && [[ ${!arg_index} = glitz ]] && \
set -- "${@:1:$(( arg_index - 1 ))}" "$shell_name" \
"${@:$(( arg_index + 1 ))}"
src=$(basename -- "$SOCKHOP_SOCK")
src=$xfer$(trunc "$(sanitize "$src")" 32)
else
src=$tty-c
fi
(( ++arg_index ))
printf -v src %s "$(field "$src" $white $black) " \
"${fgnd[$white]}$(sanitize "$arg")$normal"
elif (( stdin ))
then
src=$(field "${xfer}stdin" $white $black)
else
(( arg_index > $# )) && argerr 'Missing filename argument'
arg=${!arg_index}
[[ $arg = --- ]] && hdiv
(( ++arg_index ))
src=$(field "$file$(trunc "$(sanitize "$arg")" 32)" $white $black)
fi
# Stylize the start time and shell name
printf -v header %s "$(field "$stime$(date +%-I:%M%p)" $blue $black) " \
"${fgnd[$blue]}$(sanitize "$shell_name")$normal"
# While stylizing the shell options, ignore some redundancies
for i in "${opts[@]}"
do
case $i in
--*)
;;
-*c*|-*s*)
i=${i//c}
i=${i//s}
(( ${#i} < 2 )) && i=
esac
[[ -n $i ]] && header+=" ${fgnd[$blue]}$(sanitize "$i")$normal"
done
# Stylize the positional arguments
cols=$(twidth)
unset args
if (( $# - $arg_index < 9 ))
then
pos_args=("${@:$arg_index}")
line_len=0
[[ -n $ledge || -n $redge || -n $omargin ]] && arg_sep=$omargin || \
arg_sep=${bgnd[$cyan]}${fgnd[$black]}$vbar$normal
min_len=$(dlen "$(field "${positions[0]}")")
max_len=$(( cols - min_len ))
(( max_len < 32 )) && max_len=32
for i in $(seq 0 $(( ${#pos_args[@]} - 1 )))
do
(( line_len )) && sep=$arg_sep || sep=
sep_len=$(dlen "$sep")
arg=$(trunc "$(sanitize "${pos_args[$i]}")" $max_len)
arg=$(field "${positions[$(( i + 1 ))]}$arg" $cyan $black)
arg_len=$(dlen "$arg")
next_len=$(( line_len + sep_len + arg_len ))
if (( next_len > cols ))
then
printf -v args '%s\n%s' "$args" "$arg"
line_len=$arg_len
else
args+=$sep$arg
line_len=$next_len
fi
done
fi
# Join and output the header
line_len=$(dlen "$header")
src_len=$(dlen "$src")
if [[ -n $args ]]
then
(( cmd )) && sep=' ' || sep=$omargin
sep_len=$(dlen "$sep")
arg_len=$(dlen "$args")
if (( src_len + sep_len + arg_len > cols ))
then
(( line_len + src_len + 1 > cols )) && \
printf '%s\n' "$header" "$src" || echo "$header $src"
echo "$args"
else
(( line_len + src_len + sep_len + arg_len + 1 > cols )) && \
printf '%s\n' "$header" "$src$sep$args" ||
echo "$header $src$sep$args"
fi
else
(( line_len + src_len + 1 > cols )) && printf '%s\n' "$header" "$src" || \
echo "$header $src"
fi
# Invoke the shell
exit_status=0
start_nsecs=$(date +%s%N)
"$shell" "$@" || exit_status=$?
nsecs=$(date +%s%N)
(( nsecs > start_nsecs )) && (( nsecs -= start_nsecs )) || nsecs=0
# Output the exit status and execution time
(( exit_status )) && \
exit_status=$(field "$failure$exit_status" $red $black) || \
exit_status=$(field "${success}OK" $green $black)
if (( nsecs < 59950000000 ))
then
dsecs=$(( (nsecs + 50000000) / 100000000 ))
secs=$(( dsecs / 10 )).$(( dsecs % 10 ))
else
secs=$(( (nsecs + 500000000) / 1000000000 ))
fi
echo "$exit_status$omargin$(field "$timer${secs}s" $blue $black)"