From 894119b31ae93c97f5e4841f30305d26342616df Mon Sep 17 00:00:00 2001 From: Robert Reed Date: Sat, 21 Aug 2021 22:12:15 -0700 Subject: [PATCH 01/12] feat: holy bash spaghetti --- dactyl.sh | 576 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 576 insertions(+) create mode 100755 dactyl.sh diff --git a/dactyl.sh b/dactyl.sh new file mode 100755 index 000000000..88e294f25 --- /dev/null +++ b/dactyl.sh @@ -0,0 +1,576 @@ +#!/bin/bash + +# ******************* # +# ******************* setup ******************* # +# ******************* # + +# exit if any errors are thrown +set -e + +container="" +shellContainer=DM-shell +configContainer=DM-config +modelContainer=DM-model +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 { + printf "\n[Dactyl Manuform] Please choose an option:\n\n" + 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 + ESC=$(printf "\033") + + # 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; + } + + # 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 - $#)) + + # 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 + + local selected=0 + + # print options by overwriting the last lines + function drawOptions() { + 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 + } + + while true; do + drawOptions + + # 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)) + drawOptions + break + fi + ;; + up) ((selected--)); + if [ $selected -lt 0 ]; then selected=$(($# - 1)); fi;; + down) ((selected++)); + if [ $selected -ge $# ]; then selected=0; fi;; + esac + done + + # cursor position back to normal + cursor_to $lastrow + cursor_blink_on + echo + + return $selected +} + + +################################ +# Setup helpers +################################ + +function showHelpAndExit() { + echo "TODO: Help Menu" + exit +} + +# error on any unexpected flags or more than one positional argument +function processArgs() { + while [[ $# -gt 0 ]] + do + key="$1" + + case $key in + --build|-rm|--remove|--inspect|--run|--start|--stop) + if [[ $flags ]]; then + flags+=" $key" + else + flags=$key + fi + shift;; + -h|--help) + showHelpAndExit + ;; + *) + # 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|model)$ ]]; 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 handleRemoveImage() { + warn "This will remove docker image: $imageName" + confirmContinue "Would you like to continue?" + inform "Removing docker image: $imageName..." + docker image rm $imageName +} + +function handleInspectImage() { + inform "Checking status of image: $imageName" + docker image inspect $imageName +} + +function handleImageMenu() { + local check="Check Image Status" + local build="Reuild Image" + local remove="Remove Image" + local end="Exit" + options=("$check" "$build" "$remove" "$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;; + *) 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|-rm).*$ ]]; 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 -p 8000:8000 -it -v "$srcBind" -v "$thingsBind" $imageName + ;; + $modelContainer) + buildContainerAndExecutePythonScript dactyl_manuform.py + ;; + $configContainer) + buildContainerAndExecutePythonScript generate_configuration.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 run="Start $container Session" + local main="Main Menu" + local end="Exit" + + if ! containerExists; then + build="Build $container Container" + options=("$build" "$main" "$end") + elif containerIsRunning; then + options=("$run" "$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 && [[ $run = $result ]]; then + startShellSession + fi + exit + ;; + esac +} + +function handleContainerCLI() { + if [[ ! "$flags" ]]; then + handleContainerMenu + elif [[ "$flags" =~ ^.*(--inspect).*$ ]]; then + inspectContainer + elif [[ "$flags" =~ ^.*(--build).*$ ]]; then + handleBuildContainer + elif [[ "$flags" =~ ^.*(--run).*$ ]] && isShell; then + startShellSession + elif [[ "$flags" =~ ^.*(--start).*$ ]]; then + startContainerOrAlert + elif [[ "$flags" =~ ^.*(--stop).*$ ]]; then + handleStopContainer + elif [[ "$flags" =~ ^.*(--remove|-rm).*$ ]]; 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 +} + +################################ +# Main Menu Logic +################################ + +function handleMainMenu() { + container="" + + local imageOpt="Manage Docker Image" + local shellOpt="$shellContainer Options" + local configOpt="$configContainer Options" + local modelOpt="$modelContainer Options" + local end="Exit" + + options=("$imageOpt" "$shellOpt" "$configOpt" "$modelOpt" "$end") + + # execute in subshell so exit code doesn't exit the script + (menu "${options[@]}") && true + result="${options[$?]}" + + case $result in + $imageOpt ) handleImageMenu;; + $shellOpt|$configOpt|$modelOpt) + # remove " Options" and set as currentn container + container=$(echo "${result/ Options/}") + 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|model ) handleContainerCLI;; + * ) exitUnexpectedPositionalArgs;; + esac +else + handleMainMenu +fi From 0719f34f77f377647b3c975f01a8a7ae6ff88f59 Mon Sep 17 00:00:00 2001 From: Robert Reed Date: Sun, 22 Aug 2021 11:56:43 -0700 Subject: [PATCH 02/12] chore: on screen help * feat: menu improvements --- dactyl.sh | 68 +++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 51 insertions(+), 17 deletions(-) diff --git a/dactyl.sh b/dactyl.sh index 88e294f25..bf6259c1c 100755 --- a/dactyl.sh +++ b/dactyl.sh @@ -62,7 +62,12 @@ function exitUnexpectedFlags() { # selected index (0 for opt1, 1 for opt2 ...) function menu { - printf "\n[Dactyl Manuform] Please choose an option:\n\n" + 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 @@ -165,8 +170,32 @@ function menu { ################################ function showHelpAndExit() { - echo "TODO: Help Menu" - exit +cat << _end_of_text +[Dactyl Manuform] + +A bash CLI to manage Docker artifacts for the Dactyl Keyboard project. + +Run script without any options to use in interactive mode. + +Usage: + ./dactyl.sh + ./dactyl.sh [-h | --help | --uninstall] + ./dactyl.sh image [--build | --inspect | --remove] + ./dactyl.sh (config|model) [--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 | model) + -h --help Show this screen. + --uninstall Remove all Docker artifacts. + --build Build or rebuild 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 @@ -176,16 +205,15 @@ function processArgs() { key="$1" case $key in - --build|-rm|--remove|--inspect|--run|--start|--stop) + --build|--remove|--inspect|--session|--start|--stop) if [[ $flags ]]; then flags+=" $key" else flags=$key fi shift;; - -h|--help) - showHelpAndExit - ;; + -h|--help) showHelpAndExit;; + --uninstall) handleUninstall;; *) # all valid flags should have already been captured above if [[ $key == -* ]]; then @@ -275,10 +303,11 @@ function handleInspectImage() { function handleImageMenu() { local check="Check Image Status" - local build="Reuild Image" + local build="Rebuild Image" local remove="Remove Image" + local mainMenu="Main Menu" local end="Exit" - options=("$check" "$build" "$remove" "$end") + options=("$check" "$build" "$remove" "$mainMenu" "$end") # execute in subshell so exit code doesn't exit the script (menu "${options[@]}") && true result="${options[$?]}" @@ -287,6 +316,7 @@ function handleImageMenu() { $check) handleInspectImage;; $build) handleRebuildImage;; $remove) handleRemoveImage;; + $mainMenu) handleMainMenu;; *) exit;; esac } @@ -299,7 +329,7 @@ function handleImageCLI() { handleInspectImage elif [[ "$flags" =~ ^.*(--build).*$ ]]; then handleRebuildImage - elif [[ "$flags" =~ ^.*(--remove|-rm).*$ ]]; then + elif [[ "$flags" =~ ^.*(--remove).*$ ]]; then handleRemoveImage else exitUnexpectedFlags @@ -449,7 +479,7 @@ function handleContainerMenu() { local stop="Stop $container Container" local remove="Remove $container Container" local inspect="Inspect $container Container" - local run="Start $container Session" + local session="Start $container Session" local main="Main Menu" local end="Exit" @@ -457,7 +487,7 @@ function handleContainerMenu() { build="Build $container Container" options=("$build" "$main" "$end") elif containerIsRunning; then - options=("$run" "$inspect" "$build" "$stop" "$remove" "$main" "$end") + options=("$session" "$inspect" "$build" "$stop" "$remove" "$main" "$end") if ! isShell; then unset options[0] fi @@ -477,7 +507,7 @@ function handleContainerMenu() { $inspect) inspectContainer;; $main) handleMainMenu;; *) - if isShell && [[ $run = $result ]]; then + if isShell && [[ $session = $result ]]; then startShellSession fi exit @@ -492,13 +522,13 @@ function handleContainerCLI() { inspectContainer elif [[ "$flags" =~ ^.*(--build).*$ ]]; then handleBuildContainer - elif [[ "$flags" =~ ^.*(--run).*$ ]] && isShell; then + elif [[ "$flags" =~ ^.*(--session).*$ ]] && isShell; then startShellSession elif [[ "$flags" =~ ^.*(--start).*$ ]]; then startContainerOrAlert elif [[ "$flags" =~ ^.*(--stop).*$ ]]; then handleStopContainer - elif [[ "$flags" =~ ^.*(--remove|-rm).*$ ]]; then + elif [[ "$flags" =~ ^.*(--remove).*$ ]]; then removeContainer else exitUnexpectedFlags @@ -531,16 +561,20 @@ function handleMainMenu() { local shellOpt="$shellContainer Options" local configOpt="$configContainer Options" local modelOpt="$modelContainer Options" + local uninstallOpt="Uninstall" + local help="Show Help" local end="Exit" - options=("$imageOpt" "$shellOpt" "$configOpt" "$modelOpt" "$end") + options=("$imageOpt" "$shellOpt" "$configOpt" "$modelOpt" "$help" "$uninstallOpt" "$end") # execute in subshell so exit code doesn't exit the script (menu "${options[@]}") && true result="${options[$?]}" case $result in - $imageOpt ) handleImageMenu;; + $help) showHelpAndExit;; + $imageOpt) handleImageMenu;; + $uninstallOpt) handleUninstall;; $shellOpt|$configOpt|$modelOpt) # remove " Options" and set as currentn container container=$(echo "${result/ Options/}") From e45f1cff76ae5b6000c4efd35d6dc759359c4b3f Mon Sep 17 00:00:00 2001 From: Robert Reed Date: Sun, 22 Aug 2021 17:22:30 -0700 Subject: [PATCH 03/12] feat: uninstall all docker artifacts --- dactyl.sh | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/dactyl.sh b/dactyl.sh index bf6259c1c..c0bc41e52 100755 --- a/dactyl.sh +++ b/dactyl.sh @@ -9,8 +9,10 @@ set -e container="" shellContainer=DM-shell -configContainer=DM-config modelContainer=DM-model +configContainer=DM-config +containers=("$shellContainer" "$configContainer" "$modelContainer") + imageName=dactyl-keyboard srcBind="$(pwd)/src:/app/src" thingsBind="$(pwd)/things:/app/things" @@ -289,11 +291,15 @@ function handleRebuildImage() { 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?" - inform "Removing docker image: $imageName..." - docker image rm $imageName + removeImage } function handleInspectImage() { @@ -550,6 +556,20 @@ function promptStartShellSession() { startShellSession } +################################ +# Uninstaller +################################ + +function handleUninstall() { + for currentContainer in "${containers[@]}"; do + container="$currentContainer" + removeContainer + done + + removeImage + exit +} + ################################ # Main Menu Logic ################################ From f0cc5f2df7f169f9634d2704d3a5fd704fb4c0e8 Mon Sep 17 00:00:00 2001 From: Robert Reed Date: Sun, 22 Aug 2021 17:26:59 -0700 Subject: [PATCH 04/12] feat: prompt before uninstall --- dactyl.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dactyl.sh b/dactyl.sh index c0bc41e52..1cad28f76 100755 --- a/dactyl.sh +++ b/dactyl.sh @@ -561,6 +561,8 @@ function promptStartShellSession() { ################################ 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 From 78be9475566ee1ee797589bf2c7139e603c70a8b Mon Sep 17 00:00:00 2001 From: Robert Reed Date: Sun, 22 Aug 2021 20:09:14 -0700 Subject: [PATCH 05/12] feat: menu is ephemeral --- dactyl.sh | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/dactyl.sh b/dactyl.sh index 1cad28f76..8440bab15 100755 --- a/dactyl.sh +++ b/dactyl.sh @@ -82,8 +82,6 @@ function menu { get_cursor_row() { IFS=';' read -sdR -p $'\E[6n' ROW COL; echo ${ROW#*[}; } key_input() { local key - ESC=$(printf "\033") - # read 3 characters, 1 at a time for (( i=0; i < 3; ++i)); do read -s -n1 input 2>/dev/null >&2 @@ -105,6 +103,18 @@ function menu { 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 @@ -112,16 +122,15 @@ function menu { # 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 - local selected=0 - - # print options by overwriting the last lines - function drawOptions() { - local idx=0 + 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 @@ -133,10 +142,6 @@ function menu { fi ((idx++)) done - } - - while true; do - drawOptions # user key control input=$(key_input) @@ -147,7 +152,6 @@ function menu { # If a digit is encountered, consider it a selection (if within range) if [ $input -lt $(($# + 1)) ]; then selected=$(($input - 1)) - drawOptions break fi ;; @@ -158,10 +162,8 @@ function menu { esac done - # cursor position back to normal - cursor_to $lastrow + eraseMenu cursor_blink_on - echo return $selected } From 32963214d5e6768da2129e75c87522a824301adf Mon Sep 17 00:00:00 2001 From: Robert Reed Date: Sun, 22 Aug 2021 20:43:33 -0700 Subject: [PATCH 06/12] chore: ignore .DS_Store --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 46f869199..72b914d25 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ debug_* */__pycache__/* *~$* things/ +.DS_Store From 2ab2653cd5fd1622797771eb5dfc0b64b1ec71e2 Mon Sep 17 00:00:00 2001 From: Robert Reed Date: Sun, 22 Aug 2021 22:28:19 -0700 Subject: [PATCH 07/12] feat: automate setup via conda --- conda.sh | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100755 conda.sh diff --git a/conda.sh b/conda.sh new file mode 100755 index 000000000..6eacf9005 --- /dev/null +++ b/conda.sh @@ -0,0 +1,74 @@ +#!/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 + 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 PythonOCC..." + +conda install -c conda-forge pythonocc-core=7.4.1 -y + +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 "Updating conda dependencies..." + +conda update --all -y + +inform "Success!\n\n\tRun \"conda activate $envName\" to activate the environment." From 8d8f9c45f719781a6721a8d97fcee2ae5091f4a0 Mon Sep 17 00:00:00 2001 From: Robert Reed Date: Tue, 24 Aug 2021 20:47:08 -0700 Subject: [PATCH 08/12] fix: add solidpython to conda script --- conda.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/conda.sh b/conda.sh index 6eacf9005..8985067c8 100755 --- a/conda.sh +++ b/conda.sh @@ -67,6 +67,10 @@ inform "Installing scipy..." pip install scipy +inform "Installing solidpython..." + +pip install solidpython + inform "Updating conda dependencies..." conda update --all -y From c66bc8f10dc1d7aa7a1d2dd64bf8feb1f17925b0 Mon Sep 17 00:00:00 2001 From: Robert Reed Date: Wed, 25 Aug 2021 20:14:59 -0700 Subject: [PATCH 09/12] chore: uninstall success msg --- conda.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/conda.sh b/conda.sh index 8985067c8..33dd36d7d 100755 --- a/conda.sh +++ b/conda.sh @@ -33,6 +33,8 @@ 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 From 10c1f95b4b41f9c0ce876be5e02e7c6bf8a9f8d2 Mon Sep 17 00:00:00 2001 From: Robert Reed Date: Wed, 25 Aug 2021 21:29:32 -0700 Subject: [PATCH 10/12] feat: remove port bind * chore: match container names to other implementations * feat: add release build --- conda.sh | 6 +----- dactyl.sh | 48 ++++++++++++++++++++++++++++-------------------- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/conda.sh b/conda.sh index 33dd36d7d..16b3f58ce 100755 --- a/conda.sh +++ b/conda.sh @@ -49,15 +49,11 @@ conda create --name=$envName python=3.7 -y conda activate $envName -inform "Installing PythonOCC..." - -conda install -c conda-forge pythonocc-core=7.4.1 -y - inform "Installing CadQuery..." conda install -c conda-forge -c cadquery cadquery=2 -y -inform "Installing dataclasses_json..." +inform "Installing dataclasses-json..." pip install dataclasses-json diff --git a/dactyl.sh b/dactyl.sh index 8440bab15..7ccc387a5 100755 --- a/dactyl.sh +++ b/dactyl.sh @@ -8,10 +8,14 @@ set -e container="" +# bad practice: + # container variable names MUST match + # the positional name {positional}Container shellContainer=DM-shell -modelContainer=DM-model +runContainer=DM-run configContainer=DM-config -containers=("$shellContainer" "$configContainer" "$modelContainer") +releaseBuildContainer=DM-release-build +containers=("$shellContainer" "$configContainer" "$runContainer" "$releaseBuildContainer") imageName=dactyl-keyboard srcBind="$(pwd)/src:/app/src" @@ -179,20 +183,20 @@ cat << _end_of_text A bash CLI to manage Docker artifacts for the Dactyl Keyboard project. -Run script without any options to use in interactive mode. +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|model) [--build | --inspect | --start | --stop | --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 | model) + 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 the target container. + --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. @@ -228,7 +232,7 @@ function processArgs() { exitUnexpectedPositionalArgs $key exit 1 # the totality of accepted positional arguments - elif [[ "$key" =~ ^(image|shell|config|model)$ ]]; then + elif [[ "$key" =~ ^(image|shell|config|run|releaseBuild)$ ]]; then positional="$key" if [[ ! $key = image ]]; then key+="Container" @@ -378,14 +382,17 @@ function buildContainer() { inform "Building docker container: $container..." case $container in $shellContainer) - docker run --name $shellContainer -d -p 8000:8000 -it -v "$srcBind" -v "$thingsBind" $imageName + docker run --name $shellContainer -d -it -v "$srcBind" -v "$thingsBind" $imageName ;; - $modelContainer) + $runContainer) buildContainerAndExecutePythonScript dactyl_manuform.py ;; $configContainer) buildContainerAndExecutePythonScript generate_configuration.py ;; + $releaseBuildContainer) + buildContainerAndExecutePythonScript model_builder.py + ;; *) error "Unexpected exception. Containier: $container" exit 1;; @@ -492,7 +499,7 @@ function handleContainerMenu() { local end="Exit" if ! containerExists; then - build="Build $container Container" + build="Build and run $container Container" options=("$build" "$main" "$end") elif containerIsRunning; then options=("$session" "$inspect" "$build" "$stop" "$remove" "$main" "$end") @@ -582,14 +589,15 @@ function handleMainMenu() { container="" local imageOpt="Manage Docker Image" - local shellOpt="$shellContainer Options" - local configOpt="$configContainer Options" - local modelOpt="$modelContainer Options" + 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" "$modelOpt" "$help" "$uninstallOpt" "$end") + options=("$imageOpt" "$shellOpt" "$configOpt" "$runOpt" "$releaseOpt" "$help" "$uninstallOpt" "$end") # execute in subshell so exit code doesn't exit the script (menu "${options[@]}") && true @@ -599,9 +607,9 @@ function handleMainMenu() { $help) showHelpAndExit;; $imageOpt) handleImageMenu;; $uninstallOpt) handleUninstall;; - $shellOpt|$configOpt|$modelOpt) - # remove " Options" and set as currentn container - container=$(echo "${result/ Options/}") + $shellOpt|$configOpt|$runOpt|$releaseOpt) + # remove " Container" and set as currentn container + container=$(echo "${result/ Container/}") handleContainerMenu ;; * ) exit;; @@ -625,9 +633,9 @@ promptBuildImageIfNotExists if [[ "$positional" ]]; then case $positional in - image ) handleImageCLI;; - shell|config|model ) handleContainerCLI;; - * ) exitUnexpectedPositionalArgs;; + image) handleImageCLI;; + shell|config|run|releaseBuild) handleContainerCLI;; + *) exitUnexpectedPositionalArgs;; esac else handleMainMenu From d937d7beb2d7d898dd937cd9e024952b56607403 Mon Sep 17 00:00:00 2001 From: Robert Reed Date: Wed, 25 Aug 2021 22:40:13 -0700 Subject: [PATCH 11/12] chore: update documentation with bash scripts --- README.md | 75 ++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 66 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 6486fb727..3b5f4a537 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`~~ From 46a63d47c23da94723f0750836f5db921cb26dc9 Mon Sep 17 00:00:00 2001 From: Robert Reed Date: Tue, 31 Aug 2021 22:05:44 -0700 Subject: [PATCH 12/12] fix: add check to ensure docker daemon is running --- dactyl.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/dactyl.sh b/dactyl.sh index 7ccc387a5..43bb553be 100755 --- a/dactyl.sh +++ b/dactyl.sh @@ -255,6 +255,11 @@ function checkDocker() { 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