I have coded a speedreading training bash
script. My targed was to make some experience with bash
in a funny way.
It essentially does the following:
- Prints to screen a random number consisting of a default number of digits
- Erases that number after a default time interval
- Waits for the user input
- if the user enters a string starting with a digit (only in this case, Enter is required as the end-of-input key)
- Checks if the answer matches the number that was shown
- Updates the number of hits and misses which is always shown on the top right
- if the user hits j/k the time interval is reduced/increased
- if the user hits h/l the number of digits is reduced/increased
- if the user hits q the script exits (*)
- if the user hits any other key, does nothing
- if the user enters a string starting with a digit (only in this case, Enter is required as the end-of-input key)
- Throws the next number (i.e. it goes to step 1 in outer list)
(*) This is the only "clean" way to exit the code; in this respect, the line
while [[ "$answer${answer+xxx}" != "xxx" ]]; do # keep going if a non-null answer is provided
behaves just like while true
(and the comment is therefore wrong), as answer
is unset before while
, and it becomes set only when the [0-9]
case is entered, in which case it becomes and remains non-empty.
The script is also on GitHub.
#!/bin/bash
# terminal based speedreading tester
# links that helped me:
# - https://superuser.com/q/835824/597763
# activate extended regex
shopt -s extglob
# fall back on defaults
BLINK_DIGITS=${BLINK_DIGITS:-5}
BLINK_INTERVAL=${BLINK_INTERVAL:-0.5}
BLINK_QUIT=${BLINK_QUIT:-q}
BLINK_INCREMENT_DIGITS=${BLINK_INCREMENT_DIGITS:-l}
BLINK_DECREMENT_DIGITS=${BLINK_DECREMENT_DIGITS:-h}
BLINK_DECREMENT_TIME=${BLINK_DECREMENT_TIME:-j}
BLINK_INCREMENT_TIME=${BLINK_INCREMENT_TIME:-k}
# initialize score
right=0
wrong=0
# define functions
generate_number() {
gen=
while (( ${#gen} < $BLINK_DIGITS )); do
gen=$gen$RANDOM
done
echo ${gen:0:$BLINK_DIGITS}
}
center_vertically() {
print_header
lines=$(tput lines)
for (( i = 0; i < $lines/2; i++ )); do
printf "\n"
done
}
center_horizontally() {
width=$1
cols=$(tput cols)
for (( i = 0; i < ($cols - ${#width})/2; i++ )); do
printf " "
done
}
print_spaces() {
eval "printf ' %.0s' {1..$1}"
}
print_header() {
# prepare left part of first two lines (I can't use here-strings/documents since
# some computation has to be donw with the length of the strings, provided I want to flush
# some text to the right)
selected_time_interval="Time interval: $BLINK_INTERVAL seconds ([$BLINK_DECREMENT_TIME] reduces, [$BLINK_INCREMENT_TIME] increases)"
string_length="String length: $BLINK_DIGITS digits ([$BLINK_DECREMENT_DIGITS] removes digit, [$BLINK_INCREMENT_DIGITS] adds digit)"
# compute scores and align them to the right
trim=$(( ${#right} - ${#wrong} ))
(( trim < 0 )) && score_plus="Score: ✓ $(print_spaces ${trim#-})$right" || score_plus="Score: ✓ $right"
(( trim > 0 )) && score_minus=" ✗ $(print_spaces ${trim#-})$wrong" || score_minus=" ✗ $wrong"
max_length=$(( ${#score_plus} > ${#score_minus} ? ${#score_plus} : ${#score_minus} ))
# fill space in between and print the two lines
cols=$(tput cols)
printf "$selected_time_interval$(print_spaces $(( $cols - ${#selected_time_interval} - $max_length )))$score_plus\n"
printf "$string_length$(print_spaces $(( $cols - ${#string_length} - $max_length )))$score_minus"
echo "[$BLINK_QUIT] quits, all other keys regenerate the number"
}
redraw_screen() {
clear
center_vertically
center_horizontally $number
}
decrement_time() {
BLINK_INTERVAL="${BLINK_INTERVAL::$((${#BLINK_INTERVAL}-1))}$((${BLINK_INTERVAL:$((${#BLINK_INTERVAL}-1))} - 1))"
(( ${BLINK_INTERVAL:$((${#BLINK_INTERVAL}-1))} == 0 )) && BLINK_INTERVAL="${BLINK_INTERVAL}9"
}
increment_time() {
(( ${BLINK_INTERVAL##0.} == 9 )) && return 1
BLINK_INTERVAL=${BLINK_INTERVAL%%[1-9]*}$((${BLINK_INTERVAL##0.*(0)} + 1))
(( ${BLINK_INTERVAL:$((${#BLINK_INTERVAL} - 1))} == 0 )) && BLINK_INTERVAL=${BLINK_INTERVAL/0.0/0.} && BLINK_INTERVAL=${BLINK_INTERVAL%%0}
}
quit() {
# clear screen and exit
clear
exit 0
}
# main loop
while [[ "$answer${answer+xxx}" != "xxx" ]]; do # keep going if a non-null answer is provided
# generate number
number=$(generate_number $BLINK_DIGITS)
# show number for short time
redraw_screen
printf $number
sleep $BLINK_INTERVAL
# erase the number (go back, overwrite with spaces, go back again)
eval "printf '\b%.0s' {1..${#number}}"
eval "printf ' %.0s' {1..${#number}}"
eval "printf '\b%.0s' {1..${#number}}"
read -rn 1 first_char
case $first_char in
$BLINK_QUIT )
quit
;;
$BLINK_DECREMENT_DIGITS )
(( BLINK_DIGITS > 1 )) && (( BLINK_DIGITS-- ))
;;
$BLINK_INCREMENT_DIGITS )
(( BLINK_DIGITS < $cols - 1 )) && (( BLINK_DIGITS++ ))
;;
$BLINK_DECREMENT_TIME )
decrement_time
;;
$BLINK_INCREMENT_TIME )
increment_time
;;
[0-9] )
redraw_screen
# erase $first_char
printf '\b%.0s'
# use $first_char as a default to have it printed again
# the leading space is to allow backspacing on the first digit
# without the all leading space being erased all together
read -i " $first_char" -e answer
if (( $answer == $number )); then
(( right++ ))
else
(( wrong++ ))
fi
;;
esac
done
(( $? == 0 )) && quit || exit