summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore4
-rw-r--r--README.md75
-rwxr-xr-xconda.sh76
-rwxr-xr-xdactyl.sh647
4 files changed, 792 insertions, 10 deletions
diff --git a/.gitignore b/.gitignore
index 6cc67ea..469e6db 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,6 +14,8 @@
debug_*
*/__pycache__/*
*~$*
+things/
+.DS_Store
things/*.scad
things/*.step
-things/*.stl \ No newline at end of file
+things/*.stl
diff --git a/README.md b/README.md
index 6486fb7..3b5f4a5 100644
--- a/README.md
+++ b/README.md
@@ -15,20 +15,27 @@ As part of the effort to create a new engine I converted the code to cadquery/Op
### Docker Autobuild
![Docker Support!](./resources/docker_containers.png)
+
At the excellent suggestion of [martint17r](https://github.com/joshreve/dactyl-keyboard/issues?q=is%3Apr+author%3Amartint17r)
-I have added docker configurations with an Windows batch file to assist with getting setup.
-If there is sufficient interest I can add a .sh file as well. If you have
+I have added docker configurations with a Windows batch file to assist with getting setup.
+If you have
[docker desktop](https://www.docker.com/products/docker-desktop) installed, the batch file will create the
-dactyl-keyboard image and 3 containers: DM-run: runs the dactyl_manuform.py, DM-config: runs generate_configuration.py,
-and DM-shell: just starts an interactive session to manually run from shell (tip: run bash after entering to get the better
-shell environment). All apps bindmount the src and things directory to allow editing in the host and running in the
-container. While not exactly hard drive space efficient, this hopefully this helps those having issue getting
+dactyl-keyboard image and 4 containers:
+
+- DM-run: runs `dactyl_manuform.py`,
+- DM-config: runs `generate_configuration.py`
+- DM-shell: starts an interactive session to manually run from shell
+ - tip: run bash after entering to get a better shell environment
+- DM-release-build: runs `model_builder.py` to generate a number of keyboard variants
+
+All apps bindmount the `src` and `things` directory to allow editing in the host and running in the
+container. While not exactly hard drive space efficient, this hopefully helps those having issues getting
cadquery running and prevents local Python conflicts. It works well on my computer, but I don't use
docker often, so please let me know if you find any issues with the approach.
### Refactored
-Your settings are now created by `generate_configuration.py` or by direct modification fo the `run_config.json` file.
+Your settings are now created by `generate_configuration.py` or by direct modification of the `run_config.json` file.
This allows you to save `run_config.json` to share your configuration.
Additionally, the OpenSCAD/solid python and OpenCASCADE/cadquery versions are merged with separate helper functions
@@ -104,13 +111,61 @@ You can now have slightly better control of screw mounts. Set to `'screws_offse
## Status / Future
This is now a bit of a monster of many minds and yet continues to bear fruit. I plan to continue to use this code to try new geometries and features to share. I am still working on a new generator, but feel this one can continue to evolve and inform the other effort.
-## Generating a Design
+## Installation
+
+There are three different environments in which you can run this application. Depending on which you choose, the installation process will vary.
+
+- [Docker Environment](#docker-environment-installation)
+- [Conda Environment](#conda-environment-installation)
+- [Python Environment](#python-environment-installation)
+
+### Docker Environment Installation
+
+Running the application with Docker is the most convenient way to do so. In addition to a straightforward installation, this also allows you to generate models in the background without having to keep a shell open.
+
+*Note:* If you are using Windows, see [Docker Autobuild](#docker-autobuild).
+
+Before you proceed, ensure you have installed [Docker](https://www.docker.com/) and the `docker` command is available from your terminal.
+
+There are two tools you can use to help manage the Docker containers associated with this project.
+
+#### Make
+
+If you prefer, you can use `make` to manage the containers. Type `make help` to see the available commands.
+
+#### Bash Script
+
+The `dactyl.sh` bash script provides a CLI to manage the containers. Type `./dactyl.sh --help` to see all CLI options.
+
+In addition to the CLI you can run `./dactyl.sh` without any arguments to use an interactive menu.
+
+Upon running the script, you will be prompted to build the dactyl-keyboard Docker image.
+
+Once the image is built, you can choose which containers to run on an as-needed basis. In general, you can start, stop, rebuild, inspect, and remove the containers via the CLI/Menu.
+
+You can also remove all of the Docker artifacts by running the included uninstaller.
+
+*Tip:* Run `./dactyl.sh shell --session` to jump into a bash session inside of the shell container.
+
+### Conda Environment Installation
+
+After the Docker installation, Anaconda is the next best option. Before you begin, ensure you have installed [Anaconda](https://docs.anaconda.com/anaconda/install/index.html) and the `conda` command is available from your terminal.
+
+You can install all of the dependencies by hand, but you can automate the install by running the bash script `./conda.sh`. This will create a python 3.7 environment named `dactyl-keyboard` and install all of the required dependencies.
+
+If you would like to install into a conda environment manually, check the bash script to see all of the required commands.
+
+If you would like to remove the conda artifacts, run `./conda.sh --uninstall`.
+
+### Python Environment Installation
+
+You can install the application in a regular python environment, but it is not recommended. You will not be able to take advantage of the updated geometry generated by the CadQuery engine, as this is only available via the Docker/Anaconda installation.
**Setting up the Python environment - NEW**
* [Install Python 3.X](https://www.python.org/downloads/release/python-385/) or use your [favorite distro / platform (Anaconda)](https://www.anaconda.com/products/individual)
* It is advisable, but not necessary, to setup a virtual environment to prevent package/library incompatibility
* [Install Numpy](https://pypi.org/project/numpy/), easiest method is `pip install numpy` or `pip3 install numpy` on linux.
-* [Install dataclasses_json](https://pypi.org/project/dataclasses_json/), easiest method is `pip install numpy` or `pip3 install numpy` on linux.
+* [Install dataclasses_json](https://pypi.org/project/dataclasses_json/), easiest method is `pip install dataclasses-json` or `pip3 install dataclasses-json` on linux.
**cadquery install**
* [Install scipy](https://pypi.org/project/scipy/), easiest method is `pip install scipy` or `pip3 install scipy` on linux.
@@ -120,6 +175,8 @@ This is now a bit of a monster of many minds and yet continues to bear fruit. I
* [Install SolidPython](https://pypi.org/project/solidpython/), easiest method is `pip install solidpython` or `pip3 install solidpython` on linux.
* [Install OpenSCAD](http://www.openscad.org/)
+## Generating the design
+
**Generating the design - UPDATED**
* ~~Run `python dactyl_manuform_cadquery.py` or `python3 dactyl_manuform_cadquery.py`~~
* ~~Run `python dactyl_manuform.py` or `python3 dactyl_manuform.py`~~
diff --git a/conda.sh b/conda.sh
new file mode 100755
index 0000000..16b3f58
--- /dev/null
+++ b/conda.sh
@@ -0,0 +1,76 @@
+#!/bin/bash
+
+# exit on any errors
+set -e
+
+function inform() { echo -e "\n[INFO] $@\n"; }
+function warn() { echo -e "\n[WARN] $@\n"; }
+function error() { echo -e "\n[ERROR] $@\n"; }
+
+# 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
+}
+
+if ! which conda &> /dev/null; then
+ error "Conda not found.\n\nVisit https://docs.anaconda.com/anaconda/install/index.html for more info."
+ exit 1
+fi
+
+# Enable "conda activate" and "conda deactivate"
+eval "$(conda shell.bash hook)"
+
+envName=dactyl-keyboard
+
+if [ "$1" = "--uninstall" ]; then
+ confirmContinue "Would you like to remove the conda environment $envName?"
+ conda deactivate
+ conda env remove -n $envName
+ inform "Conda environment removed!\n\n\tRun \"conda deactivate\" to ensure the environment has been properly deactivated."
+
+ exit
+fi
+
+if conda info --envs | grep $envName &> /dev/null; then
+ warn "Conda env \"$envName\" already exists."
+ confirmContinue "Do you want to overwrite it?"
+fi
+
+inform "Creating conda environment: $envName..."
+
+conda create --name=$envName python=3.7 -y
+
+conda activate $envName
+
+inform "Installing CadQuery..."
+
+conda install -c conda-forge -c cadquery cadquery=2 -y
+
+inform "Installing dataclasses-json..."
+
+pip install dataclasses-json
+
+inform "Installing numpy..."
+
+pip install numpy
+
+inform "Installing scipy..."
+
+pip install scipy
+
+inform "Installing solidpython..."
+
+pip install solidpython
+
+inform "Updating conda dependencies..."
+
+conda update --all -y
+
+inform "Success!\n\n\tRun \"conda activate $envName\" to activate the environment."
diff --git a/dactyl.sh b/dactyl.sh
new file mode 100755
index 0000000..43bb553
--- /dev/null
+++ b/dactyl.sh
@@ -0,0 +1,647 @@
+#!/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
+
+ if ! docker image list &> /dev/null; then
+ error "Docker is not running. Please start docker and try again."
+ 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