summaryrefslogtreecommitdiff
path: root/dactyl.sh
diff options
context:
space:
mode:
Diffstat (limited to 'dactyl.sh')
-rwxr-xr-xdactyl.sh642
1 files changed, 642 insertions, 0 deletions
diff --git a/dactyl.sh b/dactyl.sh
new file mode 100755
index 0000000..7ccc387
--- /dev/null
+++ b/dactyl.sh
@@ -0,0 +1,642 @@
+#!/bin/bash
+
+# ******************* #
+# ******************* setup ******************* #
+# ******************* #
+
+# exit if any errors are thrown
+set -e
+
+container=""
+# bad practice:
+ # container variable names MUST match
+ # the positional name {positional}Container
+shellContainer=DM-shell
+runContainer=DM-run
+configContainer=DM-config
+releaseBuildContainer=DM-release-build
+containers=("$shellContainer" "$configContainer" "$runContainer" "$releaseBuildContainer")
+
+imageName=dactyl-keyboard
+srcBind="$(pwd)/src:/app/src"
+thingsBind="$(pwd)/things:/app/things"
+
+# force exit on interrupt in case we are in a menu
+function catch_interrupt() { exit 1; }
+trap catch_interrupt SIGINT
+trap catch_interrupt SIGTSTP
+
+# ******************* #
+# ******************* functions ******************* #
+# ******************* #
+
+################################
+# General Helpers
+################################
+
+function inform() {
+ echo -e "\n[INFO] $@\n"
+}
+
+function warn() {
+ echo -e "\n[WARN] $@\n"
+}
+
+function error() {
+ echo -e "\n[ERROR] $@\n"
+}
+
+function exitUnexpectedPositionalArgs() {
+ error "Unexpected positionnal argument.\n\n\tAlready had: $positional\n\n\tAnd then got: $1"
+ exit 1
+}
+
+function exitUnexpectedFlags() {
+ error "One or more flags are invalid:\n\n\tPositional: $positional\n\n\tFlags: $flags"
+ exit 1
+}
+
+################################
+# Interactive Menu
+################################
+
+# https://unix.stackexchange.com/questions/146570/arrow-key-enter-menu
+# Arguments:
+# array of options
+#
+# Return value:
+# selected index (0 for opt1, 1 for opt2 ...)
+
+function menu {
+ local header="\n[Dactyl Manuform]"
+ if [ $container ]; then
+ header+=" -- $container"
+ fi
+ header+="\n\nPlease choose an option:\n\n"
+ printf "$header"
+ options=("$@")
+
+ # helpers for terminal print control and key input
+ ESC=$(printf "\033")
+ cursor_blink_on() { printf "$ESC[?25h"; }
+ cursor_blink_off() { printf "$ESC[?25l"; }
+ cursor_to() { printf "$ESC[$1;${2:-1}H"; }
+ print_option() { printf "\t $1 "; }
+ print_selected() { printf "\t${COLOR_GREEN} $ESC[7m $1 $ESC[27m${NC}"; }
+ get_cursor_row() { IFS=';' read -sdR -p $'\E[6n' ROW COL; echo ${ROW#*[}; }
+ key_input() {
+ local key
+ # read 3 characters, 1 at a time
+ for (( i=0; i < 3; ++i)); do
+ read -s -n1 input 2>/dev/null >&2
+ # concatenate chars together
+ key+="$input"
+ # if a number is encountered, echo it back
+ if [[ $input =~ ^[1-9]$ ]]; then
+ echo $input; return;
+ # if enter, early return
+ elif [[ $input = "" ]]; then
+ echo enter; return;
+ # if we encounter something other than [1-9] or "" or the escape sequence
+ # then consider it an invalid input and exit without echoing back
+ elif [[ ! $input = $ESC && i -eq 0 ]]; then
+ return
+ fi
+ done
+
+ if [[ $key = $ESC[A ]]; then echo up; fi;
+ if [[ $key = $ESC[B ]]; then echo down; fi;
+ }
+ function cursorUp() { printf "$ESC[A"; }
+ function clearRow() { printf "$ESC[2K\r"; }
+ function eraseMenu() {
+ cursor_to $lastrow
+ clearRow
+ numHeaderRows=$(printf "$header" | wc -l)
+ numOptions=${#options[@]}
+ numRows=$(($numHeaderRows + $numOptions))
+ for ((i=0; i<$numRows; ++i)); do
+ cursorUp; clearRow;
+ done
+ }
+
+ # initially print empty new lines (scroll down if at bottom of screen)
+ for opt in "${options[@]}"; do printf "\n"; done
+
+ # determine current screen position for overwriting the options
+ local lastrow=`get_cursor_row`
+ local startrow=$(($lastrow - $#))
+ local selected=0
+
+ # ensure cursor and input echoing back on upon a ctrl+c during read -s
+ trap "cursor_blink_on; stty echo; printf '\n'; exit" 2
+ cursor_blink_off
+
+ while true; do
+ # print options by overwriting the last lines
+ local idx=0
+ for opt in "${options[@]}"; do
+ cursor_to $(($startrow + $idx))
+ # add an index to the option
+ local label="$(($idx + 1)). $opt"
+ if [ $idx -eq $selected ]; then
+ print_selected "$label"
+ else
+ print_option "$label"
+ fi
+ ((idx++))
+ done
+
+ # user key control
+ input=$(key_input)
+
+ case $input in
+ enter) break;;
+ [1-9])
+ # If a digit is encountered, consider it a selection (if within range)
+ if [ $input -lt $(($# + 1)) ]; then
+ selected=$(($input - 1))
+ break
+ fi
+ ;;
+ up) ((selected--));
+ if [ $selected -lt 0 ]; then selected=$(($# - 1)); fi;;
+ down) ((selected++));
+ if [ $selected -ge $# ]; then selected=0; fi;;
+ esac
+ done
+
+ eraseMenu
+ cursor_blink_on
+
+ return $selected
+}
+
+
+################################
+# Setup helpers
+################################
+
+function showHelpAndExit() {
+cat << _end_of_text
+[Dactyl Manuform]
+
+A bash CLI to manage Docker artifacts for the Dactyl Keyboard project.
+
+Run the script without any flags to use the interactive menu.
+
+Usage:
+ ./dactyl.sh
+ ./dactyl.sh [-h | --help | --uninstall]
+ ./dactyl.sh image [--build | --inspect | --remove]
+ ./dactyl.sh (config|run|releaseBuild) [--build | --inspect | --start | --stop | --remove]
+ ./dactyl.sh shell [--build | --inspect | --session | --start | --stop | --remove]
+
+Options:
+ positional Target the image or a particular container (shell | config | run | releaseBuild)
+ -h --help Show this screen.
+ --uninstall Remove all Docker artifacts.
+ --build Build (or rebuild) and run the target container.
+ --inspect Show "docker inspect" results for the target container.
+ --start Start or restart the target container.
+ --stop Stop the target container.
+ --remove Remove the target container.
+ --session Start a shell session in the shell container.
+_end_of_text
+exit
+}
+
+# error on any unexpected flags or more than one positional argument
+function processArgs() {
+ while [[ $# -gt 0 ]]
+ do
+ key="$1"
+
+ case $key in
+ --build|--remove|--inspect|--session|--start|--stop)
+ if [[ $flags ]]; then
+ flags+=" $key"
+ else
+ flags=$key
+ fi
+ shift;;
+ -h|--help) showHelpAndExit;;
+ --uninstall) handleUninstall;;
+ *)
+ # all valid flags should have already been captured above
+ if [[ $key == -* ]]; then
+ error "Unknwon flag: $key"
+ exit 1
+ # if we already have a positional argument we shouldn't get another
+ elif [[ "$positional" ]]; then
+ exitUnexpectedPositionalArgs $key
+ exit 1
+ # the totality of accepted positional arguments
+ elif [[ "$key" =~ ^(image|shell|config|run|releaseBuild)$ ]]; then
+ positional="$key"
+ if [[ ! $key = image ]]; then
+ key+="Container"
+ container=$(echo "${!key}")
+ fi
+ shift
+ else
+ error "Unknown positional arg: \"$key\""
+ exit 1
+ fi
+ ;;
+ esac
+ done
+}
+
+# installing docker is out of scope
+# so if it isn't found, inform user and exit
+function checkDocker() {
+ if ! which docker &> /dev/null; then
+ error "Docker is not installed.\n\n\tPlease visit https://www.docker.com/products/docker-desktop for more information."
+ exit 1
+ fi
+}
+
+# exit unless user responds with yes
+function confirmContinue() {
+ while true; do
+ read -p "$@ [y/n]" yn
+ case $yn in
+ [Yy]* ) break;;
+ [Nn]* ) exit 0;;
+ * ) error "Please answer yes or no.";;
+ esac
+ done
+}
+
+################################
+# Image Logic
+################################
+
+function imageExists() {
+ docker image list | grep "$imageName" &> /dev/null
+}
+
+function buildImage() {
+ inform "Building docker image: $imageName..."
+ docker build -t dactyl-keyboard -f docker/Dockerfile .
+}
+
+function promptBuildImageIfNotExists() {
+ if ! imageExists; then
+ inform "Docker image not found: $imageName"
+ confirmContinue "Would you like to build it now?"
+ buildImage
+ fi
+}
+
+# image will always exist if we are here
+function handleRebuildImage() {
+ warn "Docker image already exists: $imageName"
+ confirmContinue "Would you like to overwrite it?"
+ buildImage
+}
+
+function removeImage() {
+ inform "Removing docker image: $imageName..."
+ docker image rm $imageName
+}
+
+function handleRemoveImage() {
+ warn "This will remove docker image: $imageName"
+ confirmContinue "Would you like to continue?"
+ removeImage
+}
+
+function handleInspectImage() {
+ inform "Checking status of image: $imageName"
+ docker image inspect $imageName
+}
+
+function handleImageMenu() {
+ local check="Check Image Status"
+ local build="Rebuild Image"
+ local remove="Remove Image"
+ local mainMenu="Main Menu"
+ local end="Exit"
+ options=("$check" "$build" "$remove" "$mainMenu" "$end")
+ # execute in subshell so exit code doesn't exit the script
+ (menu "${options[@]}") && true
+ result="${options[$?]}"
+
+ case $result in
+ $check) handleInspectImage;;
+ $build) handleRebuildImage;;
+ $remove) handleRemoveImage;;
+ $mainMenu) handleMainMenu;;
+ *) exit;;
+ esac
+}
+
+# if we made it this far, image is confirmed to exist
+function handleImageCLI() {
+ if [[ ! "$flags" ]]; then
+ handleImageMenu
+ elif [[ "$flags" =~ ^.*(--inspect).*$ ]]; then
+ handleInspectImage
+ elif [[ "$flags" =~ ^.*(--build).*$ ]]; then
+ handleRebuildImage
+ elif [[ "$flags" =~ ^.*(--remove).*$ ]]; then
+ handleRemoveImage
+ else
+ exitUnexpectedFlags
+ fi
+}
+
+################################
+# Container Helpers
+################################
+
+function containerExists() {
+ docker container list -a | grep "$container" &> /dev/null
+}
+
+function containerIsRunning() {
+ if ! containerExists "$container"; then
+ return 1
+ fi
+
+ docker container inspect $container | grep '"Status": "running",' &> /dev/null
+}
+
+function isShell() {
+ test $container = $shellContainer
+}
+
+function promptIfShell() {
+ if isShell; then
+ promptStartShellSession
+ fi
+}
+
+function buildContainerAndExecutePythonScript() {
+ docker run --name $container -d -v "$srcBind" -v "$thingsBind" $imageName python3 -i $1
+}
+
+function buildContainer() {
+ inform "Building docker container: $container..."
+ case $container in
+ $shellContainer)
+ docker run --name $shellContainer -d -it -v "$srcBind" -v "$thingsBind" $imageName
+ ;;
+ $runContainer)
+ buildContainerAndExecutePythonScript dactyl_manuform.py
+ ;;
+ $configContainer)
+ buildContainerAndExecutePythonScript generate_configuration.py
+ ;;
+ $releaseBuildContainer)
+ buildContainerAndExecutePythonScript model_builder.py
+ ;;
+ *)
+ error "Unexpected exception. Containier: $container"
+ exit 1;;
+ esac
+ echo
+}
+
+function buildContainerIfNotExists() {
+ if ! containerExists; then
+ warn "Container not found: $container"
+ confirmContinue "Would you like to build it now?"
+ buildContainer
+ fi
+}
+
+function startContainer() {
+ docker container start $container &> /dev/null
+}
+
+function promptStartContainerIfNotRunning() {
+ buildContainerIfNotExists
+ if ! containerIsRunning; then
+ warn "Container is not running: $container"
+ confirmContinue "Would you like to start it now?"
+ startContainer
+ fi
+}
+
+function startContainerIfNotRunning() {
+ buildContainerIfNotExists
+ if ! containerIsRunning; then
+ inform "Starting docker container: $container"
+ startContainer
+ fi
+}
+
+function startContainerOrAlert() {
+ if containerIsRunning; then
+ inform "Container is already running: $shellContainer"
+ else
+ startContainerIfNotRunning
+ fi
+
+ if isShell; then
+ promptStartShellSession
+ fi
+}
+
+function stopContainer() {
+ if containerIsRunning; then
+ inform "Stopping docker container: $container..."
+ docker container stop $container &> /dev/null
+ docker container wait $container &> /dev/null
+ fi
+}
+
+function handleStopContainer() {
+ if ! containerExists; then
+ warn "Docker container does not exist: $container"
+ elif ! containerIsRunning; then
+ inform "Container is already stopped: $container"
+ else
+ stopContainer
+ fi
+}
+
+function removeContainer() {
+ if containerExists; then
+ stopContainer
+ inform "Removing docker container: $container..."
+ docker container rm $container &> /dev/null
+ fi
+}
+
+function inspectContainer() {
+ if ! containerExists; then
+ inform "Container \"$container\" does not exist."
+ confirmContinue "Would you like to build it?"
+ buildContainer
+ fi
+
+ docker container inspect $container
+}
+
+function handleBuildContainer() {
+ if containerExists; then
+ warn "Container already exists: $container"
+ confirmContinue "Would you like to overwrite it?"
+ removeContainer
+ fi
+
+ buildContainer
+ promptIfShell
+}
+
+function handleContainerMenu() {
+ local build="Rebuild $container Container"
+ local start="Start $container Container"
+ local stop="Stop $container Container"
+ local remove="Remove $container Container"
+ local inspect="Inspect $container Container"
+ local session="Start $container Session"
+ local main="Main Menu"
+ local end="Exit"
+
+ if ! containerExists; then
+ build="Build and run $container Container"
+ options=("$build" "$main" "$end")
+ elif containerIsRunning; then
+ options=("$session" "$inspect" "$build" "$stop" "$remove" "$main" "$end")
+ if ! isShell; then
+ unset options[0]
+ fi
+ else
+ options=("$inspect" "$build" "$start" "$remove" "$main" "$end")
+ fi
+
+ # execute in subshell so exit code doesn't exit the script
+ (menu "${options[@]}") && true
+ result="${options[$?]}"
+
+ case $result in
+ $build) handleBuildContainer;;
+ $start) startContainerOrAlert;;
+ $stop) handleStopContainer;;
+ $remove) removeContainer;;
+ $inspect) inspectContainer;;
+ $main) handleMainMenu;;
+ *)
+ if isShell && [[ $session = $result ]]; then
+ startShellSession
+ fi
+ exit
+ ;;
+ esac
+}
+
+function handleContainerCLI() {
+ if [[ ! "$flags" ]]; then
+ handleContainerMenu
+ elif [[ "$flags" =~ ^.*(--inspect).*$ ]]; then
+ inspectContainer
+ elif [[ "$flags" =~ ^.*(--build).*$ ]]; then
+ handleBuildContainer
+ elif [[ "$flags" =~ ^.*(--session).*$ ]] && isShell; then
+ startShellSession
+ elif [[ "$flags" =~ ^.*(--start).*$ ]]; then
+ startContainerOrAlert
+ elif [[ "$flags" =~ ^.*(--stop).*$ ]]; then
+ handleStopContainer
+ elif [[ "$flags" =~ ^.*(--remove).*$ ]]; then
+ removeContainer
+ else
+ exitUnexpectedFlags
+ fi
+}
+
+################################
+# Shell Specific Logic
+################################
+
+function startShellSession() {
+ promptStartContainerIfNotRunning
+ inform "Starting session in container: $shellContainer\n\n\tType \"exit\" to terminate the session."
+ docker exec -it $shellContainer /bin/bash
+}
+
+function promptStartShellSession() {
+ confirmContinue "Would you like to start a shell session?"
+ startShellSession
+}
+
+################################
+# Uninstaller
+################################
+
+function handleUninstall() {
+ warn "This will remove all containers and images."
+ confirmContinue "Are you sure you want to continue?"
+ for currentContainer in "${containers[@]}"; do
+ container="$currentContainer"
+ removeContainer
+ done
+
+ removeImage
+ exit
+}
+
+################################
+# Main Menu Logic
+################################
+
+function handleMainMenu() {
+ container=""
+
+ local imageOpt="Manage Docker Image"
+ local shellOpt="$shellContainer Container"
+ local configOpt="$configContainer Container"
+ local releaseOpt="$releaseBuildContainer Container"
+ local runOpt="$runContainer Container"
+ local uninstallOpt="Uninstall"
+ local help="Show Help"
+ local end="Exit"
+
+ options=("$imageOpt" "$shellOpt" "$configOpt" "$runOpt" "$releaseOpt" "$help" "$uninstallOpt" "$end")
+
+ # execute in subshell so exit code doesn't exit the script
+ (menu "${options[@]}") && true
+ result="${options[$?]}"
+
+ case $result in
+ $help) showHelpAndExit;;
+ $imageOpt) handleImageMenu;;
+ $uninstallOpt) handleUninstall;;
+ $shellOpt|$configOpt|$runOpt|$releaseOpt)
+ # remove " Container" and set as currentn container
+ container=$(echo "${result/ Container/}")
+ handleContainerMenu
+ ;;
+ * ) exit;;
+ esac
+}
+
+# ******************* #
+# ******************* main ******************* #
+# ******************* #
+
+# figure out why we're running the script
+processArgs $@
+
+# exit if `docker` command not available
+checkDocker
+
+# make sure the base image has been built
+promptBuildImageIfNotExists
+
+# main switchboard to act depending on which positionl arg was passed
+
+if [[ "$positional" ]]; then
+ case $positional in
+ image) handleImageCLI;;
+ shell|config|run|releaseBuild) handleContainerCLI;;
+ *) exitUnexpectedPositionalArgs;;
+ esac
+else
+ handleMainMenu
+fi