From 2730f2f61a42e7b5b73715932a6194adf9742489 Mon Sep 17 00:00:00 2001 From: Mihail Ivanchev Date: Sun, 21 Jan 2024 14:07:28 +0100 Subject: [PATCH 1/3] Big refactoring to remove continuous forking and added FIGlet rendering using a custom font. --- stopwatch.flf | 316 ++++++++++++++++++++++++++++++++++++++++++++++++++ sw | 269 +++++++++++++++++++++++++++++++++++------- 2 files changed, 540 insertions(+), 45 deletions(-) create mode 100644 stopwatch.flf diff --git a/stopwatch.flf b/stopwatch.flf new file mode 100644 index 0000000..15e40ce --- /dev/null +++ b/stopwatch.flf @@ -0,0 +1,316 @@ +flf2a$ 3 3 3 -1 9 0 0 0 +Font Author: Mihail Ivanchev +License: MIT License +This font was created specifically for sw in order to display the stopwatch +in a more visually appealing "digital" look compared to the default terminal +font. It contains only digits and the characters ":.,". I was inspired by +the figfont "ANSI Regular" but wanted something smaller. The font uses +full block, upper half block and lower half block characters only. +Because of this there is no real baseline but the middle of the 3rd row is +thought of as baseline. +$@ +$@ +$@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +$ @ +$ @ +█$@@ +@ +@ +@@ +$ @ +$ @ +▀$@@ +@ +@ +@@ +█▀█$@ +█ █$@ +▀▀▀$@@ +▄█$ @ + █$ @ +▀▀▀$@@ +▀▀█$@ +█▀▀$@ +▀▀▀$@@ +▀▀█$@ +▀▀█$@ +▀▀▀$@ +█ █$@ +▀▀█$@ + ▀$@@ +█▀▀$@ +▀▀█$@ +▀▀▀$@ +█▀▀$@ +█▀█$@ +▀▀▀$@ +▀▀█$@ + █ $@ + ▀ $@ +█▀█$@ +█▀█$@ +▀▀▀$@ +█▀█$@ +▀▀█$@ +▀▀▀$@ +▄$@ +▄$@ + $@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ +@ +@ +@@ diff --git a/sw b/sw index 3efdd0f..2b233ef 100755 --- a/sw +++ b/sw @@ -1,65 +1,244 @@ #!/usr/bin/env bash set -e -# Usage: -# sw -# - start a stopwatch from 0, save start time -# sw [-r|--resume] -# - start a stopwatch from the last saved start time (or current time if no last saved start time exists) -# - "-r" stands for --resume +VERSION=1.0.2 -function finish { - tput cnorm # Restore cursor - exit 0 +function echo_err { + echo "$@" 1>&2 } -trap finish EXIT +if [[ ${BASH_VERSINFO[0]} -lt 5 ]]; then + echo_err \ +"Error: your Bash is too old ($BASH_VERSION); this program requires at least +Bash 5.0." + exit 1 +fi -# Use GNU date if possible as it's most likely to have nanoseconds available. -if hash gdate 2>/dev/null; then - GNU_DATE=gdate -elif date --version | grep 'GNU coreutils' >/dev/null; then - GNU_DATE=date +function show_version { + echo "sw $VERSION" + echo "" + echo "Licensed under the MIT license. Written by Cory Klein and others." +} + +function show_usage { + echo_fn=$1 + $echo_fn "Usage: sw [OPTION]..." + $echo_fn "Display a continously updated stopwatch. + + -f If possible, use the specified FIGlet font to render the + stopwatch; FIGlet must be in PATH + -r If possible, use the timestamp in \$HOME/.sw as an initial time + instead of the current time + -h Print this help and exit + -v Print version information and exit +" +} + +show_version= +show_usage= +resume= + +while getopts ":vhrf:" opt +do + case "${opt}" in + v) show_version=y;; + h) show_usage=y;; + f) ;; + r) ;; + ?) echo_err "sw: invalid option \"-$OPTARG\"."; + show_usage echo_err + exit 1;; + esac +done + +if [[ $show_version = y ]]; then + show_version + exit 0 +fi +if [[ $show_usage = y ]]; then + show_usage echo + exit 0 fi -function datef { - if [[ -z "$GNU_DATE" ]]; then - date "$@" +OPTIND=1 + +while getopts ":vhrf:" opt +do + case "${opt}" in + f) STOPWATCH_FONT="$OPTARG";; + r) resume=y;; + esac +done + +RENDER_CMD=(cat) +PADDING_TOP= +PADDING_LEFT= + +if [[ -n "$STOPWATCH_FONT" ]]; then + if ! command -v figlet &>/dev/null; then + echo_err \ +"WARNING: stopwatch font '$STOPWATCH_FONT' will not be used because FIGlet +was not found in PATH." + else + if ! figlet -f "$STOPWATCH_FONT" "" >/dev/null; then + echo_err \ +"WARNING: stopwatch font '$STOPWATCH_FONT' will not be used because FIGlet +reported an error" + else + RENDER_CMD=(figlet -f "$STOPWATCH_FONT") + PADDING_TOP="\n" + PADDING_LEFT=" " + fi + fi +fi + +# Implements a signed subtraction for positive numbers A and B with equal +# number of digits after the decimal separator separator. The result is placed +# in _subtract_result and DOESN'T have a decimal separator (a multiplication +# by 10eN is implied). This function exists because it's unknown whether all +# Bash implementations could deal with EPOCH timestamps of microsecond +# precision. +function _subtract() { + sign= + difference=0 + borrow=0 + multiplier=1 + + _subtract_result=0 + + minuend=$1 + subtrahend=$2 + + # Check if the the result is negative because the subtrahend is larger + # (has more digits before the decimal separator and one of them is >0). + + for ((ii = ${#subtrahend} - ${#minuend} - 1; ii >= 0; ii--)); do + if [[ ${subtrahend:ii:1} -ne 0 ]]; then + minuend=$2 + subtrahend=$1 + sign=- + break + fi + done + + # Loop over the digits right to left and subtract ignoring the decimal + # separator. + + length=${#minuend} + length=$((length--)) + + for ((ii = -1; ii >= -length; ii--)); do + dm=${minuend:ii:1} + + if [[ "$dm" = "." ]]; then + continue + fi + + if [[ $borrow -eq 1 ]]; then + dm=$((dm-1)) + borrow=0 + fi + if [[ $ii -lt ${#subtrahend} ]]; then + ds=${subtrahend:ii:1} else - $GNU_DATE "$@" + ds=0 fi + if [[ $dm -lt $ds ]]; then + borrow=1 + dm=$((dm+10)) + fi + difference=$((difference + (dm - ds) * multiplier)) + multiplier=$((multiplier * 10)) + done + + # If we needed to borrow for the last digit the result is actually negative + # subtract anew. + + if [[ $borrow -eq 1 ]]; then + _subtract $2 $1 + sign=- + fi + + _subtract_result=${sign}${difference} } -# Display nanoseconsd only if supported -if datef +%N | grep -q N 2>/dev/null; then - DATE_FORMAT="+%H:%M:%S" -else - DATE_FORMAT="+%H:%M:%S.%N" - NANOS_SUPPORTED=true -fi +stty_status=$(stty -a | grep -oh '\b-\?echo\b' || echo -n "echo") -tput civis # hide cursor +function finish { + # Restore the cursor and terminal echoing. + tput cnorm + stty $stty_status + [ -e /proc/$$/fd/3 ] && exec 3<&- + exit 0 +} -# If -r is passed, use saved start time from ~/.sw -if [[ "$1" == "-r" || "$1" == "--resume" ]]; then - if [[ ! -f $HOME/.sw ]]; then - datef +%s > $HOME/.sw +trap finish EXIT + +# Hide the cursor. +tput civis + +# Disable echoing of characters inputted by the user (like return etc.) that +# could affect the vizualization. +stty -echo + +# Open a file descriptor to a process that never generates any input. This +# is used to hack a sleep-like logic using read because read doesn't create +# a new process. +exec 3<> <(:) + +# Initialize the timestamp as close to the main loop as possible. + +start_time=$EPOCHREALTIME +if [[ "$resume" == "y" ]]; then + if [[ ! -f "$HOME/.sw" ]]; then + echo "$start_time" > "$HOME/.sw" + else + prev_time=$(cat "$HOME/.sw") + if [[ ! "$prev_time" =~ ^[1-9][0-9]*\.[0-9]{6}$ ]]; then + echo_err \ +"WARNING: Will not use timestamp from file \"$HOME/.sw\" because it's of +invalid form." + else + _subtract $start_time $prev_time + if [[ $_subtract_result -lt 0 ]]; then + echo_err \ +"WARNING: Will not use timestamp from file \"$HOME/.sw\" because it refers to +the future." + else + start_time=$prev_time + fi fi - START_TIME=$(cat $HOME/.sw) + fi else - START_TIME=$(datef +%s) - echo -n $START_TIME > $HOME/.sw + echo "$start_time" > "$HOME/.sw" fi -# GNU date accepts the input date differently than BSD -if [[ -z "$GNU_DATE" ]]; then - DATE_INPUT="-v-${START_TIME}S" -else - DATE_INPUT="--date now-${START_TIME}sec" -fi +echo -ne "$PADDING_TOP" + +while true; do + _subtract $EPOCHREALTIME $start_time + ms=$((_subtract_result / 1000)) + printf "%02d:%02d:%02d.%03d\n\n" \ + $(((ms / (1000 * 60 * 60)) % 60)) \ + $(((ms / (1000 * 60)) % 60)) \ + $(((ms / 1000) %60)) \ + $((ms % 1000)) + read -t 0.05 -u 3 || true +done | stdbuf -oL "${RENDER_CMD[@]}" | { + reset=0 + lines=0 + while IFS='' read line; do + if [[ -z "$line" ]]; then + reset=1 + else + if [[ $reset -eq 1 ]]; then + printf "\033[%dA" $lines + reset=0 + lines=0 + fi + echo "${PADDING_LEFT}${line}" + lines=$((lines + 1)) + fi + done +} -while [ true ]; do - STOPWATCH=$(TZ=UTC datef $DATE_INPUT $DATE_FORMAT | ( [[ "$NANOS_SUPPORTED" ]] && sed 's/.\{7\}$//' || cat ) ) - printf "\r\e%s" $STOPWATCH - sleep 0.03 -done From 74191576fc35f11f4cceeb04df28a88186d8c3fd Mon Sep 17 00:00:00 2001 From: Mihail Ivanchev Date: Sun, 21 Jan 2024 20:19:20 +0100 Subject: [PATCH 2/3] Workaround for process substitution issues on MacOS. --- sw | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/sw b/sw index 2b233ef..e80dca9 100755 --- a/sw +++ b/sw @@ -184,7 +184,12 @@ stty -echo # Open a file descriptor to a process that never generates any input. This # is used to hack a sleep-like logic using read because read doesn't create # a new process. -exec 3<> <(:) +exec 3<> <(:) 2>/dev/null || { + fifo=$(mktemp -u) + mkfifo -m 700 "$fifo" + exec 3<>"$fifo" + rm "$fifo" +} # Initialize the timestamp as close to the main loop as possible. From 9eb3aaa1eb9230535e8154ccf18efc25650c5005 Mon Sep 17 00:00:00 2001 From: Mihail Ivanchev Date: Mon, 22 Jan 2024 12:41:42 +0100 Subject: [PATCH 3/3] Ignoring all output from exec. --- sw | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sw b/sw index e80dca9..edae3ab 100755 --- a/sw +++ b/sw @@ -184,7 +184,7 @@ stty -echo # Open a file descriptor to a process that never generates any input. This # is used to hack a sleep-like logic using read because read doesn't create # a new process. -exec 3<> <(:) 2>/dev/null || { +{ exec 3<> <(:); } 2>/dev/null || { fifo=$(mktemp -u) mkfifo -m 700 "$fifo" exec 3<>"$fifo"