Created
September 14, 2018 06:21
-
-
Save ErnWong/3371b8cfd0940d907715ed5c6156459d to your computer and use it in GitHub Desktop.
SE206 Assignment 1 - Name Sayer bash script
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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