Skip to content

Instantly share code, notes, and snippets.

@ErnWong
Created September 14, 2018 06:21
Show Gist options
  • Save ErnWong/3371b8cfd0940d907715ed5c6156459d to your computer and use it in GitHub Desktop.
Save ErnWong/3371b8cfd0940d907715ed5c6156459d to your computer and use it in GitHub Desktop.
SE206 Assignment 1 - Name Sayer bash script
#!/usr/bin/env bash
# Creation parameters
CREATION_EXTENSION="mkv"
CREATION_DURATION=5
# Folder structure
DIR_WORKING=$(pwd)
DIR_CREATIONS="$DIR_WORKING/creations"
DIR_TEMPORARY="$DIR_WORKING/tmp"
FILE_LOGS="$DIR_WORKING/errors.log"
# Terminal escape sequences
# TODO: Improve terminal-portability
UPARROW=$'\e[A'
DOWNARROW=$'\e[B'
LEFTARROW=$'\e[C'
RIGHTARROW=$'\e[D'
ENTER=$'\n'
BACKSPACE=$'\b'
DELETE=$'\x7f'
ESCAPE=$'\x1b'
# Colour configuration
COLOR_SCREEN_BG="4"
COLOR_RAW_BG="0"
COLOR_RAW_FG="7"
COLOR_WINDOW_BG="7"
COLOR_WINDOW_FG="0"
COLOR_HIGHLIGHT_FG="0"
COLOR_HIGHLIGHT_BG="3"
# Turn off monitor mode - suppress job control messages
set +m
#
#
# Program entry, exits, and traps
#
#
function init
{
if ! validate_screen_size
then
echo Your terminal size is too small for NameSayer to run. Aborting.
exit 1
fi
screen_init
IFS=$'\n\b'
screen_width=$(tput cols)
screen_height=$(tput lines)
(( screen_midx = $screen_width / 2 ))
(( screen_midy = $screen_height / 2 ))
mkdir -p $DIR_CREATIONS
state_machine_loop
}
function teardown
{
window_progressbar_stop
IFS=$
screen_teardown
}
trap resize SIGWINCH
function resize
{
screen_width=$(tput cols)
screen_height=$(tput lines)
(( screen_midx = $screen_width / 2 ))
(( screen_midy = $screen_height / 2 ))
if ! validate_screen_size
then
env_window
teardown
echo "Screen too small. Aborting..."
exit 1
fi
}
trap quit INT
function quit
{
screen_clear
state_quit
}
#
#
# Screen - terminal configurations
#
#
function validate_screen_size
{
[ "$(tput lines)" -ge 10 ] && [ "$(tput cols)" -ge 70 ]
}
function screen_init
{
tput init
tput smcup
tput civis
stty_original=$(stty -g)
stty -echo raw
}
function screen_teardown
{
stty $stty_original
tput cnorm
tput reset
tput rmcup
}
function screen_clear
{
tput setab "$COLOR_SCREEN_BG"
tput clear
}
#
#
# Ext - External commands for audio/video manipulation
#
#
function ext_record_audio
{
ffmpeg \
-f alsa \
-i default \
-t $CREATION_DURATION \
audio.wav
}
function ext_replay_audio
{
ffplay \
-autoexit \
-i audio.wav
}
function ext_create_video
{
ffmpeg \
-f lavfi \
-i color=black:d=$CREATION_DURATION \
-vf "drawtext='text=$selected_creation':fontsize=10:fontcolor=white:x=(w-text_w)/2:y=(h-text_h)/2" \
video.mp4
}
function ext_merge_audio_video
{
ffmpeg \
-i audio.wav \
-i video.mp4 \
-c copy \
$selected_file
}
function ext_play_creation
{
ffplay \
-autoexit \
-i $selected_file
}
function ext_wrap_error
{
local error_title=$1
shift
local error_message=$1
shift
local cleanup_command=$1
shift
local command=$@
if ! $command &> $FILE_LOGS
then
$cleanup_command
screen_clear
window_set 4 70 "$error_title"
window_draw
window_text 2 2 "$error_message"
window_text 3 2 "Press any key to see what happened."
wait_key
screen_teardown
cat $FILE_LOGS
printf "\n\rPress any key to continue..."
wait_key
printf "\n"
screen_init
return 1
else
rm $FILE_LOGS
$cleanup_command
fi
}
#
#
# Tmpdir - scratchpad directory access
#
#
function tmpdir_start
{
rm -rf "$DIR_TEMPORARY"
mkdir -p "$DIR_TEMPORARY"
cd "$DIR_TEMPORARY"
}
function tmpdir_end
{
cd "$DIR_WORKING"
rm -rf "$DIR_TEMPORARY"
}
#
#
# Window and env - Windowing and user interface
#
#
function window_set
{
window_height=$1
window_width=$2
window_title=$3
window_title_len=${#3}
(( window_title_left = $screen_midx - ($window_title_len / 2) ))
(( window_top = $screen_midy - ($window_height / 2) ))
(( window_left = $screen_midx - ($window_width / 2) ))
(( window_bottom = $window_top + $window_height ))
(( window_right = $window_left + $window_width ))
}
function env_window
{
tput sgr0
tput setab "$COLOR_WINDOW_BG"
tput setaf "$COLOR_WINDOW_FG"
}
function env_highlight
{
tput sgr0
tput setab "$COLOR_HIGHLIGHT_BG"
tput setaf "$COLOR_HIGHLIGHT_FG"
}
function window_draw
{
env_window
tput cup $window_top $window_left
for y in $(seq "$window_top" "$window_bottom")
do
tput cup "$y" "$window_left"
for x in $(seq $window_width)
do
printf " "
done
done
tput rev
tput cup "$window_top" "$window_left"
for x in $(seq $window_width)
do
printf " "
done
tput cup "$window_top" "$window_title_left"
printf "$window_title"
tput sgr0
}
function window_cursor
{
tput cup $(( $1 + $window_top )) $(( $2 + $window_left ))
}
function window_box
{
env_window
(( row1 = $window_top + $1 ))
(( col1 = $window_left + $2 ))
(( row2 = $window_top + $3 ))
(( col2 = $window_left + $4 ))
tput cup $row1 $col1
for x in $(seq $(( $col1 + 1 )) $col2)
do
printf "─"
done
tput cup $row2 $col1
for x in $(seq $(( $col1 + 1 )) $col2)
do
printf "─"
done
for y in $(seq $(( $row1 + 1 )) $row2)
do
tput cup $y $col1
printf "│"
tput cup $y $col2
printf "│"
done
tput cup $row1 $col1
printf "┌"
tput cup $row2 $col2
printf "┘"
tput cup $row2 $col1
printf "└"
tput cup $row1 $col2
printf "┐"
}
function window_text
{
case "$1" in
"hl")
env_highlight
shift
;;
*)
env_window
;;
esac
local row
local col
(( row = $1 + $window_top ))
(( col = $2 + $window_left ))
tput cup "$row" "$col"
printf "$3"
}
function window_progressbar_start
{
local row
local col_start
local col_end
(( row = $1 + $window_top ))
(( col_start = $2 + $window_left ))
local subdivisions=$3
local duration=$4
(( col_end = $col_start + $duration * $subdivisions ))
local step="$(bc <<< "scale = 3; 1 / $subdivisions")"
env_window
tput cup "$(($row - 1))" "$col_start"
printf "0s"
tput cup "$(($row - 1))" "$(($col_end - 1))"
printf "${duration}s"
tput cup "$row" "$col_start"
for t in $(seq "$col_start" "$col_end")
do
printf "░"
done
{
env_window
for col in $(seq "$col_start" "$col_end")
do
tput cup "$(($row - 1))" "$col_start"
printf "$(( ($col - $col_start) / $subdivisions ))s"
tput cup "$row" "$col"
printf "█"
sleep "$step"
done
sleep 1000
} &
window_progressbar_jobpid="$!"
}
function window_progressbar_stop
{
if jobs -p | grep "$window_progressbar_jobpid" &> /dev/null
then
kill "$window_progressbar_jobpid" &> /dev/null
fi
}
function window_clear
{
(( row = $1 + $window_top ))
(( col_start = $2 + $window_left ))
(( col_end = $3 + $window_left ))
tput cup "$row" "$col_start"
for x in $(seq $col_start $col_end)
do
printf " "
done
}
function window_clear_row
{
tput cup "$(( $window_top + $1 ))" "$window_left"
for x in $(seq $window_left $(($window_right - 1)))
do
printf " "
done
}
#
#
# Keyboard input
#
#
function read_key
{
local key
local key1
local key2
local key3
read -sN1 key
read -sN1 -t 0.0001 key1 2> /dev/null
read -sN1 -t 0.0001 key2 2> /dev/null
read -sN1 -t 0.0001 key3 2> /dev/null
if [[ "$key" =~ [^a-zA-Z0-9] ]]
then
key+=$key1$key2$key3
fi
case $key in
$UPARROW)
key=UPARROW
;;
$DOWNARROW)
key=DOWNARROW
;;
$LEFTARROW)
key=LEFTARROW
;;
$RIGHTARROW)
key=RIGHTARROW
;;
$ENTER|"")
key=ENTER
;;
$DELETE|$BACKSPACE)
key=BACKSPACE
;;
$ESCAPE)
key=ESCAPE
;;
esac
echo "$key"
}
function wait_key
{
local gobble=$(read_key)
# Redirecting to /dev/null does strange things when ctrl-c ing
}
#
#
# Creation specific helpers
#
#
function list_creations
{
pushd "$DIR_CREATIONS" > /dev/null
find -name "$1*.$CREATION_EXTENSION" | sort
popd > /dev/null
}
function filename_to_name
{
local ext_length_including_dot=$(( ${#CREATION_EXTENSION} + 1 ))
echo ${1:2:-$ext_length_including_dot}
}
function select_creation
{
local selection_mode=$2
local h=6
if [ "$selection_mode" == "existing" ]
then
h=$screen_height
if [ -z "$(list_creations)" ]
then
window_set 4 50 "$1"
window_draw
window_text 2 2 "There are no creations."
window_text 3 2 "Press any key to continue..."
wait_key
return 1
fi
fi
window_set "$h" 70 "$1"
window_draw
window_text 3 2 "Enter Name:"
window_box 2 18 4 66
local input_text
local prev_last_dirty_row=8
while true
do
window_clear 3 19 65
window_clear_row 5
window_text 3 20 "$input_text"
window_text 5 20 "Press escape to cancel"
if [ $selection_mode == "existing" ]
then
# Show matching creation names if selecting an existing creation
window_clear_row 7
for clearing_row in $(seq 9 "$prev_last_dirty_row")
do
window_clear_row $clearing_row
done
local row=9
local matches=$(list_creations $input_text)
if [ "$matches" ]
then
local num_matches=$(wc -l <<< "$matches")
window_text 7 2 "Found $num_matches matching creations:"
for match in $matches
do
[ "$row" -ge "$screen_height" ] && break
window_text "$row" 5 $(filename_to_name $match)
(( row++ ))
done
else
window_text 7 2 "No matching creations found."
fi
prev_last_dirty_row="$row"
fi
window_cursor 3 $(( 20 + ${#input_text} ))
tput cnorm
key=$(read_key)
tput civis
case $key in
"ENTER")
local creation_file="$DIR_CREATIONS/$input_text.$CREATION_EXTENSION"
if [ -z $input_text ]
then
window_clear_row 5
window_text 5 20 "Please enter a name. "
sleep 1
elif [ ! -f $creation_file ] && [ "$selection_mode" == "existing" ]
then
window_clear_row 5
window_text 5 20 "Creation not found."
sleep 1
elif [ -f $creation_file ] && [ "$selection_mode" == "new" ]
then
window_clear_row 5
window_text 5 20 "Creation already exists."
sleep 1
else
selected_creation="$input_text"
selected_file="$creation_file"
break
fi
;;
"BACKSPACE")
[ "$input_text" ] && input_text=${input_text::-1}
window_text 3 $(( 20 + ${#input_text} )) " "
;;
"ESCAPE")
selected_creation=
selected_file=
return 1
break
;;
[a-zA-Z]|" ")
if [ "${#input_text}" -ge 45 ]
then
window_clear_row 5
window_text 5 20 "Name too long."
sleep 1
else
input_text+=$key
fi
;;
esac
done
screen_clear
}
#
#
# Program states
#
#
function state_list
{
local creations=$(list_creations)
if [ -z "$creations" ]
then
screen_clear
window_set 4 50 "List of Creations"
window_draw
window_text 2 2 "There are no creations."
window_text 3 2 "Press any key to continue..."
wait_key
return
fi
local num_creations=$(wc -l <<< "$creations")
window_set "$screen_height" 70 "List of Creations"
window_draw
window_text 2 2 "There are $num_creations creations:"
local row=4
for filename in $creations
do
if [ "$(($row + 2))" -ge "$screen_height" ]
then
screen_clear
window_set 5 50 "List of Creations"
window_draw
window_text 2 2 "There are $num_creations creations."
window_text 3 2 "Too many creations to fit onto screen."
window_text 4 2 "Would you like to view in a pager? [y/n]"
[ "$(read_key)" != "y" ] && return
screen_teardown
{
for filename in $creations
do
filename_to_name "$filename"
done
} | less
screen_init
return
fi
window_text "$row" 5 $(filename_to_name $filename)
(( row++ ))
done
(( row++ ))
window_text "$row" 2 "Press any key to continue..."
wait_key
}
function state_play
{
select_creation "Select Creation to Play" existing || return
window_set 3 40 "Playing Creation"
window_draw
window_text 2 2 "A window should popup."
ext_wrap_error \
"Error during playback" \
"Oops! Something went wrong while playing the creation." \
"" \
ext_play_creation
}
function state_delete
{
select_creation "Select Creation to Delete" existing || return
window_set 4 70 "Confirm Deletion"
window_draw
window_text 2 2 "About to delete \"$selected_creation\""
window_text 3 2 "Proceed? [y/n]"
while true
do
local key=$(read_key)
[ $key == "y" ] && break
[ $key == "n" ] && return
done
screen_clear
window_set 4 70 "Deleting Creation"
window_draw
window_text 2 2 "Deleting \"$selected_creation\"..."
rm $selected_file
window_text 2 2 "Deleting \"$selected_creation\"... done."
window_text 3 2 "Press any key to continue"
wait_key
}
function state_create
{
select_creation "Choose Name for New Creation" new || return
tmpdir_start
ext_wrap_error \
"Error while generating video" \
"Oops! Something went wrong while creating video." \
"" \
ext_create_video || { tmpdir_end; return; }
while true
do
window_set 5 70 "About to Create \"$selected_creation\""
window_draw
window_text 2 2 "You will need to record the audio for this creation."
window_text 3 2 "Press escape to cancel."
window_text 4 2 "Press any other key to begin recording (lasts five seconds)."
local key=$(read_key)
[ "$key" == "ESCAPE" ] && { tmpdir_end; return; }
screen_clear
window_set 5 70 "Recording \"$selected_creation\""
window_draw
window_progressbar_start 3 15 8 5
ext_wrap_error \
"Error while recording" \
"Oops! Something went wrong while recording the audio." \
window_progressbar_stop \
ext_record_audio || { tmpdir_end; return; }
screen_clear
window_set 5 70 "Review \"$selected_creation\""
window_draw
window_text 3 2 "Would you like to hear the recorded audio? [y/n]..."
if [ "$(read_key)" == "y" ]
then
window_draw
window_progressbar_start 3 10 8 5
ext_wrap_error \
"Error with playback" \
"Oops! Something went wrong while playing the audio." \
window_progressbar_stop \
ext_replay_audio || { tmpdir_end; return; }
fi
screen_clear
window_set 7 70 "Confirm \"$selected_creation\""
window_draw
local sel_row=0
local decision
while true
do
window_text $([ $sel_row == 0 ] && echo hl) 2 6 " (k)eep audio and save creation"
window_text $([ $sel_row == 1 ] && echo hl) 3 6 " (r)edo audio "
window_text $([ $sel_row == 2 ] && echo hl) 4 6 " (c)ancel creation "
window_text 6 2 "Select an option [k/r/c]..."
local key=$(read_key)
if [ "$key" == "ENTER" ]
then
key="S$sel_row"
fi
case "${key^^}" in
"S0"|"K")
decision=keep
break
;;
"S1"|"R")
decision=redo
break
;;
"S2"|"C"|"ESCAPE")
tmpdir_end
return
;;
"UPARROW"|"K") (( sel_row = (sel_row + 2) % 3 )) ;;
"DOWNARROW"|"J") (( sel_row = (sel_row + 1) % 3 )) ;;
esac
done
if [ "$decision" == "keep" ]
then
break
fi
screen_clear
rm audio.wav
done
ext_wrap_error \
"Error while merging" \
"Oops! Something went wrong while finalizing the creation." \
"" \
ext_merge_audio_video
tmpdir_end
screen_clear
window_set 4 70 "Created \"$selected_creation\""
window_draw
window_text 2 2 "Your new creation has been created."
window_text 3 2 "Press any key to continue..."
wait_key
}
function state_quit
{
window_set 3 40 "NameSayer"
window_draw
window_text 2 2 "NameSayer is closing. Goodbye."
sleep 1
teardown
exit 0
}
function state_menu
{
window_set 11 60 "Welcome to NameSayer"
window_draw
local sel_row=0
while true
do
window_text 2 2 "Please select from one of the following options:"
window_text $([ $sel_row == 0 ] && echo hl) 4 6 " (l)ist existing creations "
window_text $([ $sel_row == 1 ] && echo hl) 5 6 " (p)lay an existing creation "
window_text $([ $sel_row == 2 ] && echo hl) 6 6 " (d)elete an existing creation "
window_text $([ $sel_row == 3 ] && echo hl) 7 6 " (c)reate a new creation "
window_text $([ $sel_row == 4 ] && echo hl) 8 6 " (q)uit authoring tool "
window_text 10 2 "Enter a selection [l/p/d/c/q]..."
local key=$(read_key)
if [ "$key" == "ENTER" ]
then
key="S$sel_row"
fi
case "${key^^}" in
"S0"|"L") state_next=state_list ;;
"S1"|"P") state_next=state_play ;;
"S2"|"D") state_next=state_delete ;;
"S3"|"C") state_next=state_create;;
"S4"|"Q"|"ESCAPE") state_next=state_quit;;
"UPARROW"|"K") (( sel_row = (sel_row + 4) % 5 )) ;;
"DOWNARROW"|"J") (( sel_row = (sel_row + 1) % 5 )) ;;
esac
if [ "$state_next" != "state_menu" ]
then
break
fi
done
}
function state_machine_loop
{
state_next=state_menu
local state_current
while true
do
state_current=$state_next
state_next=state_menu
screen_clear
$state_current
done
}
# And we begin...
init
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment