Reputation: 81
I have a task scheduler which runs a bash script. The task first opens a GIT Bash terminal, an opening message is shown ("The script is about to start in 60 seconds.") and runs a script at the end of that countdown.
Now, I would like to improve user experience, allowing him/her to stop/resume the countdown or (without any intervention) leaving the script running automatically. So, this is the procedure flow:
I've tried to use the read -p
but it is not good for me: I don't want the user action to fire something but to stop/pause the countdown instead (and then resume it).
Upvotes: 2
Views: 1188
Reputation: 44957
Update history:
Pausable Countdown
Combining some hints from similar questions here and some external resources about how to read single character (e.g. here, otherwise everywhere on the internet), and adding an additional loop for resumption, this is what I came up with:
#!/bin/bash
# Starts a pausable/resumable countdown.
#
# Starts the countdown that runs for the
# specified number of seconds. The
# countdown can be paused and resumed by pressing the
# spacebar.
#
# The countdown can be sped up by holding down any button
# that is no the space bar.
#
# Expects the number of seconds as single
# argument.
#
# @param $1 number of seconds for the countdown
function resumableCountdown() {
local totalSeconds=$1
while (( $totalSeconds > 0 ))
do
IFS= read -n1 -t 1 -p "Countdown $totalSeconds seconds (press <Space> to pause)" userKey
echo ""
if [ "$userKey" == " " ]
then
userKey=not_space
while [ "$userKey" != " " ]
do
IFS= read -n1 -p "Paused, $totalSeconds seconds left (press <Space> to resume)" userKey
echo ""
done
elif [ -n "$userKey" ]
then
echo "You pressed '$userKey', press <Space> to pause!"
fi
totalSeconds=$((totalSeconds - 1))
done
}
# little test
resumableCountdown 60
This can be saved and run as a stand-alone script. The function can be reused elsewhere. It pauses / resumes with SPACE
, because this seemed to be more intuitive to me, because it's how it works e.g. in video-players embedded in browsers.
The countdown can also be sped up by pressing keys other than the space bar (that's a feature).
Issuing a warning message and waiting for a pausable timeout
The following variation implements a pausable timeout, which prints nothing but the initial warning message, unless the user pauses or resumes the (internal) countdown by pressing the spacebar:
# Prints a warning and then waits for a
# timeout. The timeout is pausable.
#
# If the user presses the spacebar, the
# internal countdown for the timeout is
# paused. It can be resumed by pressing
# spacebar once again.
#
# @param $1 timeout in seconds
# @param $2 warning message
warningWithPausableTimeout() {
local remainingSeconds="$1"
local warningMessage="$2"
echo -n "$warningMessage $remainingSeconds seconds (Press <SPACE> to pause)"
while (( "$remainingSeconds" > 0 ))
do
readStartSeconds="$SECONDS"
pressedKey=""
IFS= read -n1 -t "$remainingSeconds" pressedKey
nowSeconds="$SECONDS"
readSeconds=$(( nowSeconds - readStartSeconds ))
remainingSeconds=$(( remainingSeconds - readSeconds ))
if [ "$pressedKey" == " " ]
then
echo ""
echo -n "Paused ($remainingSeconds seconds remaining, press <SPACE> to resume)"
pressedKey=""
while [ "$pressedKey" != " " ]
do
IFS= read -n1 pressedKey
done
echo ""
echo "Resumed"
fi
done
echo ""
}
warningWithPausableTimeout 10 "Program will end in"
echo "end."
Pausable countdown that updates the same line
This is a countdown similar to the first one, but it takes only a single line. Relies on echo -e
for erasing and overriding previously printed messages.
# A pausable countdown that repeatedly updates the same line.
#
# Repeatedly prints the message, the remaining time, and the state of
# the countdown, overriding the previously printed messages.
#
# @param $1 number of seconds for the countdown
# @param $2 message
singleLinePausableCountdown() {
local remainingSeconds="$1"
local message="$2"
local state="run"
local stateMessage=""
local pressedKey=""
while (( $remainingSeconds > 0 ))
do
if [ "$state" == "run" ]
then
stateMessage="[$remainingSeconds sec] Running, press <SPACE> to pause"
else
stateMessage="[$remainingSeconds sec] Paused, press <SPACE> to continue"
fi
echo -n "$message $stateMessage"
pressedKey=""
if [ "$state" == "run" ]
then
IFS= read -n1 -t 1 pressedKey
if [ "$pressedKey" == " " ]
then
state="pause"
else
remainingSeconds=$(( remainingSeconds - 1 ))
fi
else
IFS= read -n1 pressedKey
if [ "$pressedKey" == " " ]
then
state="run"
fi
fi
echo -ne "\033[1K\r"
done
echo "$message [Done]"
}
This one might behave strangely if the line is longer than the console width (it does not erase the line completely).
Unsorted collection of hints for anyone who tries to make sth. similar:
IFS= read -n1
reads single characterread -t <seconds>
sets time-out for read
. Once the timeout expires, read
exits with non-zero, and sets variable to empty.$SECONDS
measures the time from the start of the script in seconds.echo -n
, then it can be erased and reset with echo -ne "\033[1K\r"
.Upvotes: 4