diff --git a/.forgejo/workflows/build.yaml b/.forgejo/workflows/build.yaml index 8ec7b17..dfb2dd8 100644 --- a/.forgejo/workflows/build.yaml +++ b/.forgejo/workflows/build.yaml @@ -8,7 +8,7 @@ on: jobs: build: - runs-on: [host-amd] + runs-on: [docker] env: BRANCH: ${{ github.ref_name }} COMMIT: ${{ github.sha }} @@ -20,9 +20,7 @@ jobs: - name: Compile tux-dock run: | - cmake -S . -B build - cmake --build build -j - cp build/tux-dock ./tux-dock + g++ -std=c++17 main.cpp -o tux-dock chmod +x tux-dock echo "🎉 tux-dock compiled and marked executable!" @@ -44,3 +42,4 @@ jobs: name: tux-dock-linux-x86_64-${{ env.BRANCH }}-${{ env.COMMIT }} path: tux-dock-linux-x86_64-${{ env.BRANCH }}-*.tar.gz + diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 567609b..0000000 --- a/.gitignore +++ /dev/null @@ -1 +0,0 @@ -build/ diff --git a/CMakeLists.txt b/CMakeLists.txt deleted file mode 100644 index 2b8b1ec..0000000 --- a/CMakeLists.txt +++ /dev/null @@ -1,30 +0,0 @@ -cmake_minimum_required(VERSION 3.16) - -project(tux-dock LANGUAGES CXX) - -set(CMAKE_CXX_STANDARD 17) -set(CMAKE_CXX_STANDARD_REQUIRED ON) -set(CMAKE_CXX_EXTENSIONS OFF) - -include(FetchContent) - -set(FTXUI_BUILD_DOCS OFF CACHE BOOL "" FORCE) -set(FTXUI_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) -set(FTXUI_BUILD_TESTS OFF CACHE BOOL "" FORCE) - -FetchContent_Declare( - ftxui - GIT_REPOSITORY https://github.com/ArthurSonzogni/FTXUI.git - GIT_TAG v5.0.0 -) - -FetchContent_MakeAvailable(ftxui) - -add_executable(tux-dock main.cpp) - -target_link_libraries(tux-dock - PRIVATE - ftxui::screen - ftxui::dom - ftxui::component -) diff --git a/README.md b/README.md index 62050b2..da37e7c 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,6 @@ It offers a clean, interactive menu for common Docker operations like pulling im - **C++17 or newer** compiler (e.g., `g++`, `clang++`) -- **CMake 3.16+** - **Docker Engine** installed and running (tested on Debian 12/13, Alpine Linux, and Arch Linux) @@ -37,13 +36,12 @@ It offers a clean, interactive menu for common Docker operations like pulling im git clone https://mentalnet.xyz/forgejo/markmental/tuxdock.git cd tuxdock -# Configure & build (FTXUI is fetched automatically) -cmake -S . -B build -cmake --build build -j +# Build the binary +g++ -std=c++17 main.cpp -o tux-dock # Run it (requires Docker permissions) -sudo ./build/tux-dock -``` +sudo ./tux-dock +```` Prefer a prebuilt binary? The CI runner publishes build artifacts for the latest commits at: https://mentalnet.xyz/forgejo/markmental/tuxdock/actions diff --git a/compile.sh b/compile.sh index f5a6b58..ec09bb9 100755 --- a/compile.sh +++ b/compile.sh @@ -1,3 +1,3 @@ #!/bin/sh -cmake -S . -B build && cmake --build build -j && echo "tux-dock successfully compiled!" +g++ -std=c++17 main.cpp -o tux-dock && echo "tux-dock successfully compiled!" diff --git a/main.cpp b/main.cpp index 6e9bc8e..6f8bea1 100644 --- a/main.cpp +++ b/main.cpp @@ -1,302 +1,358 @@ -#include -#include -#include -#include -#include -#include -#include #include -#include -#include #include -#include -#include #include +#include +#include +#include +#include +#include +#include -#include -#include -#include +using namespace std; class DockerManager { public: - struct ContainerInfo { - std::string id; - std::string name; - std::string status; - std::string ports; - bool running = false; - }; - - std::vector getContainerList() const; - std::vector> getImageList() const; - - bool pullImage(const std::string& image, std::string& message) const; - bool runContainerInteractive(const std::string& image, - const std::vector& ports, - std::string& message) const; - bool startInteractive(const std::string& containerId, std::string& message) const; - bool startDetached(const std::string& containerId, std::string& message) const; - bool deleteImage(const std::string& imageId, std::string& message) const; - bool stopContainer(const std::string& containerId, std::string& message) const; - bool removeContainer(const std::string& containerId, std::string& message) const; - bool execShell(const std::string& containerId, std::string& message) const; - bool execDetachedCommand(const std::string& containerId, - const std::string& command, - std::string& message) const; - bool spinUpMySQL(const std::string& port, - const std::string& password, - const std::string& version, - std::string& message) const; - bool showContainerIP(const std::string& containerId, std::string& message) const; - bool createDockerfile(const std::string& baseImage, - const std::string& bashScriptPath, - std::string outputFile, - const std::string& imageName, - std::string& message) const; + void pullImage(); + void runContainerInteractive(); + void listContainers() const; + void listImages() const; + void startInteractive(); + void startDetached(); + void deleteImage(); + void stopContainer(); + void removeContainer(); + void execShell(); + void execDetachedCommand(); + void createDockerfile(); + void spinUpMySQL(); + void showContainerIP(); private: - static bool runCommand(const std::string& cmd, bool quiet = true); - static std::string shellEscape(const std::string& value); + static void runCommand(const string& cmd); + vector> getContainerList() const; + string selectContainer(const string& prompt); + /* NEW helper – retrieves all images */ + vector> getImageList() const; }; -bool DockerManager::runCommand(const std::string& cmd, bool quiet) { - std::string command = cmd; - if (quiet) { - command += " > /dev/null 2>&1"; - } - const int code = std::system(command.c_str()); - return code == 0; +// ---------------- Core Utility ---------------- + +void DockerManager::runCommand(const string& cmd) { + system(cmd.c_str()); } -std::string DockerManager::shellEscape(const std::string& value) { - std::string escaped = "'"; - for (char c : value) { - if (c == '\'') { - escaped += "'\\''"; - } else { - escaped += c; - } - } - escaped += "'"; - return escaped; -} - -std::vector> DockerManager::getImageList() const { - std::vector> images; - std::array buffer{}; - std::string result; +vector> DockerManager::getImageList() const { + vector> images; + array buffer{}; + string result; FILE* pipe = popen("docker images --format '{{.ID}} {{.Repository}}:{{.Tag}}'", "r"); - if (!pipe) { - return images; - } - while (fgets(buffer.data(), static_cast(buffer.size()), pipe) != nullptr) { + if (!pipe) return images; + while (fgets(buffer.data(), buffer.size(), pipe) != nullptr) { result = buffer.data(); - std::stringstream ss(result); - std::string id; - std::string repoTag; + stringstream ss(result); + string id, repoTag; ss >> id >> repoTag; - if (!id.empty() && !repoTag.empty()) { + if (!id.empty() && !repoTag.empty()) images.emplace_back(id, repoTag); - } } pclose(pipe); return images; } -std::vector DockerManager::getContainerList() const { - std::vector containers; - std::array buffer{}; - std::string result; +vector> DockerManager::getContainerList() const { + vector> containers; + array buffer{}; + string result; - FILE* pipe = popen("docker ps -a --format '{{.ID}}\t{{.Names}}\t{{.Status}}\t{{.Ports}}'", "r"); - if (!pipe) { - return containers; - } + FILE* pipe = popen("docker ps -a --format '{{.ID}} {{.Names}}'", "r"); + if (!pipe) return containers; - while (fgets(buffer.data(), static_cast(buffer.size()), pipe) != nullptr) { + while (fgets(buffer.data(), buffer.size(), pipe) != nullptr) { result = buffer.data(); - while (!result.empty() && (result.back() == '\n' || result.back() == '\r')) { - result.pop_back(); - } - - std::stringstream ss(result); - ContainerInfo info; - std::getline(ss, info.id, '\t'); - std::getline(ss, info.name, '\t'); - std::getline(ss, info.status, '\t'); - std::getline(ss, info.ports); - - if (!info.id.empty() && !info.name.empty()) { - info.running = info.status.rfind("Up", 0) == 0; - containers.push_back(info); - } + stringstream ss(result); + string id, name; + ss >> id >> name; + if (!id.empty() && !name.empty()) + containers.emplace_back(id, name); } pclose(pipe); return containers; } -bool DockerManager::pullImage(const std::string& image, std::string& message) const { - if (image.empty()) { - message = "Please provide an image name."; - return false; - } - const bool ok = runCommand("docker pull " + shellEscape(image)); - message = ok ? "Image pulled successfully." : "Could not pull that image."; - return ok; -} - -bool DockerManager::runContainerInteractive(const std::string& image, - const std::vector& ports, - std::string& message) const { - if (image.empty()) { - message = "Please choose an image first."; - return false; +string DockerManager::selectContainer(const string& prompt) { + auto containers = getContainerList(); + if (containers.empty()) { + cout << "No containers available.\n"; + return ""; } - std::string cmd = "docker run -it "; - for (const auto& port : ports) { - cmd += "-p " + shellEscape(port) + " "; + cout << "\nAvailable Containers:\n"; + int i = 1; + for (const auto& c : containers) + cout << i++ << ". " << c.second << " (" << c.first.substr(0, 12) << ")\n"; + + int choice; + cout << prompt << " (1-" << containers.size() << "): "; + cin >> choice; + + if (choice < 1 || choice > static_cast(containers.size())) { + cout << "Invalid selection.\n"; + return ""; } - cmd += shellEscape(image) + " /bin/sh"; - const bool ok = runCommand(cmd, false); - message = ok ? "Interactive container session finished." : "Could not start that container session."; - return ok; + return containers[choice - 1].first; } -bool DockerManager::startInteractive(const std::string& containerId, std::string& message) const { - const bool ok = runCommand("docker start -ai " + shellEscape(containerId), false); - message = ok ? "Interactive container session finished." : "Could not start that container interactively."; - return ok; +// ---------------- Docker Actions ---------------- + +void DockerManager::pullImage() { + const vector images = {"debian:stable", "ubuntu:noble", "rockylinux:9.3", "alpine:latest"}; + + cout << "\nSelect Docker image to pull:\n"; + for (size_t i = 0; i < images.size(); ++i) cout << i + 1 << ". " << images[i] << "\n"; + cout << images.size() + 1 << ". Enter custom image\n"; + cout << "Choose an option (1-" << images.size() + 1 << "): "; + + int choice; + cin >> choice; + + string image; + if (choice >= 1 && choice <= static_cast(images.size())) { + image = images[choice - 1]; + } else if (choice == static_cast(images.size()) + 1) { + cout << "Enter custom Docker image name: "; + cin >> image; + } else { + cout << "Invalid selection. Aborting.\n"; + return; + } + + cout << "Pulling image: " << image << "\n"; + runCommand("docker pull " + image); } -bool DockerManager::startDetached(const std::string& containerId, std::string& message) const { - const bool ok = runCommand("docker start " + shellEscape(containerId)); - message = ok ? "Container started in detached mode." : "Could not start that container."; - return ok; +void DockerManager::runContainerInteractive() { + // Get list of available images + auto images = getImageList(); + string image; + + if (images.empty()) { + cout << "No Docker images found. Please pull an image first.\n"; + return; + } + + // Display available images + cout << "\nAvailable Docker Images:\n"; + int idx = 1; + for (const auto& img : images) { + cout << idx++ << ". " << img.second << " (" << img.first.substr(0, 12) << ")\n"; + } + + // Prompt user to select or enter custom image + cout << idx << ". Enter custom Docker image name\n"; + cout << "Choose an option (1-" << images.size() << " or " << idx << "): "; + + int choice; + cin >> choice; + + if (choice >= 1 && choice <= static_cast(images.size())) { + // User selected an existing image + image = images[choice - 1].second; + } else if (choice == idx) { + // User wants to enter custom image name + cout << "Enter custom Docker image name: "; + cin >> image; + } else { + cout << "Invalid selection. Aborting.\n"; + return; + } + + // Continue with port mapping + int portCount; + cout << "How many port mappings? "; + cin >> portCount; + vector ports; + for (int i = 0; i < portCount; ++i) { + string port; + cout << "Enter mapping #" << (i + 1) + << " (format host:container, e.g., 8080:80): "; + cin >> port; + ports.push_back("-p " + port); + } + string portArgs; + for (const auto& p : ports) portArgs += p + " "; + cout << "\nPort Forwarding Explanation:\n" + << " '-p hostPort:containerPort' exposes the container’s port to the host.\n" + << " Example: '-p 8080:80' allows access via http://localhost:8080\n\n"; + runCommand("docker run -it " + portArgs + image + " /bin/sh"); } -bool DockerManager::deleteImage(const std::string& imageId, std::string& message) const { - const bool ok = runCommand("docker rmi " + shellEscape(imageId)); - message = ok ? "Image deleted." : "Could not delete that image. It may still be in use."; - return ok; +void DockerManager::listContainers() const { runCommand("docker ps -a"); } +void DockerManager::listImages() const { runCommand("docker images"); } + +void DockerManager::startInteractive() { + string id = selectContainer("Select container to start interactively"); + if (!id.empty()) runCommand("docker start -ai " + id); } -bool DockerManager::stopContainer(const std::string& containerId, std::string& message) const { - const bool ok = runCommand("docker stop " + shellEscape(containerId)); - message = ok ? "Container stopped." : "Could not stop that container."; - return ok; +void DockerManager::startDetached() { + string id = selectContainer("Select container to start detached"); + if (!id.empty()) runCommand("docker start " + id); } -bool DockerManager::removeContainer(const std::string& containerId, std::string& message) const { - const bool ok = runCommand("docker rm " + shellEscape(containerId)); - message = ok ? "Container removed." : "Could not remove that container."; - return ok; +void DockerManager::deleteImage() { + auto images = getImageList(); + if (images.empty()) { + cout << "No Docker images found.\n"; + return; + } + + cout << "\nAvailable Images:\n"; + int idx = 1; + for (const auto& img : images) { + cout << idx++ << ". " << img.second << " (" << img.first.substr(0, 12) << ")\n"; + } + + int choice; + cout << "Select image to delete (1-" << images.size() << "): "; + cin >> choice; + + if (choice < 1 || choice > static_cast(images.size())) { + cout << "Invalid selection.\n"; + return; + } + + const string& id = images[choice - 1].first; + cout << "Deleting image: " << images[choice - 1].second + << " (ID: " << id << ") ...\n"; + runCommand("docker rmi " + id); } -bool DockerManager::execShell(const std::string& containerId, std::string& message) const { - const bool ok = runCommand("docker exec -it " + shellEscape(containerId) + " /bin/sh", false); - message = ok ? "Shell session finished." : "Could not open a shell in that container."; - return ok; +void DockerManager::stopContainer() { + string id = selectContainer("Select container to stop"); + if (!id.empty()) runCommand("docker stop " + id); } -bool DockerManager::execDetachedCommand(const std::string& containerId, - const std::string& command, - std::string& message) const { +void DockerManager::removeContainer() { + string id = selectContainer("Select container to remove"); + if (!id.empty()) runCommand("docker rm " + id); +} + +void DockerManager::execShell() { + string id = selectContainer("Select running container for shell access"); + if (!id.empty()) runCommand("docker exec -it " + id + " /bin/sh"); +} + +void DockerManager::execDetachedCommand() { + string id = selectContainer("Select container to run command in (detached)"); + if (id.empty()) return; + + // Flush any leftover newline before using getline + cin.ignore(numeric_limits::max(), '\n'); + + string command; + cout << "Enter command to execute inside the container: "; + getline(cin, command); + if (command.empty()) { - message = "Please provide a command to run."; - return false; + cout << "No command entered. Aborting.\n"; + return; } - const std::string cmd = "docker exec -d " + shellEscape(containerId) + " /bin/sh -c " + - shellEscape(command); - const bool ok = runCommand(cmd); - message = ok ? "Command dispatched in detached mode." : "Could not run that command."; - return ok; + string escapedCommand; + escapedCommand.reserve(command.size() * 2); + for (char c : command) { + if (c == '"' || c == '\\') + escapedCommand += '\\'; + escapedCommand += c; + } + + cout << "Executing command in detached mode...\n"; + runCommand("docker exec -d " + id + " /bin/sh -c \"" + escapedCommand + "\""); + cout << "Command dispatched.\n"; } -bool DockerManager::spinUpMySQL(const std::string& port, - const std::string& password, - const std::string& version, - std::string& message) const { - const std::string cmd = "docker run -p " + shellEscape(port) + - " --name mysql-container -e MYSQL_ROOT_PASSWORD=" + - shellEscape(password) + " -d " + shellEscape("mysql:" + version); - const bool ok = runCommand(cmd); - message = ok ? "MySQL container launched." : "Could not launch MySQL. Check the version and port mapping."; - return ok; +void DockerManager::spinUpMySQL() { + string port, password, version; + cout << "Enter port mapping (e.g., 3306:3306): "; + cin >> port; + cout << "Enter MySQL root password: "; + cin >> password; + cout << "Enter MySQL version tag (e.g., 8): "; + cin >> version; + + cout << "\nLaunching MySQL container (accessible via localhost:" + << port.substr(0, port.find(':')) << ")\n"; + runCommand("docker run -p " + port + + " --name mysql-container -e MYSQL_ROOT_PASSWORD=" + password + + " -d mysql:" + version); } -bool DockerManager::showContainerIP(const std::string& containerId, std::string& message) const { - const std::string command = "docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' " + - shellEscape(containerId); +// ---------------- Parsed IP Feature ---------------- - std::array buffer{}; - std::string ip; +void DockerManager::showContainerIP() { + string id = selectContainer("Select container to view IP"); + if (id.empty()) return; + + string command = "docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' " + id; + + array buffer{}; + string ip; FILE* pipe = popen(command.c_str(), "r"); if (!pipe) { - message = "Could not inspect that container."; - return false; + cout << "Failed to inspect container.\n"; + return; } - if (fgets(buffer.data(), static_cast(buffer.size()), pipe) != nullptr) { + if (fgets(buffer.data(), buffer.size(), pipe) != nullptr) { ip = buffer.data(); } pclose(pipe); - while (!ip.empty() && (ip.back() == '\n' || ip.back() == '\r' || ip.back() == ' ' || ip.back() == '\t')) { - ip.pop_back(); - } - - if (ip.empty()) { - message = "No IP address found. The container may be stopped."; - return false; - } - - message = "Container IP address: " + ip; - return true; + if (ip.empty() || ip == "\n") + cout << "No IP address found (container may be stopped or not attached to a network).\n"; + else + cout << "Container IP Address: " << ip; } -bool DockerManager::createDockerfile(const std::string& baseImage, - const std::string& bashScriptPath, - std::string outputFile, - const std::string& imageName, - std::string& message) const { - if (baseImage.empty() || bashScriptPath.empty()) { - message = "Base image and script path are required."; - return false; +void DockerManager::createDockerfile() { + string baseImage, bashScriptPath, outputFile, imageName; + cout << "Enter base Docker image (e.g., ubuntu:22.04): "; + cin >> baseImage; + + cout << "Enter path to bash script to convert (e.g., setup.sh): "; + cin >> bashScriptPath; + + if (!filesystem::exists(bashScriptPath)) { + cout << "Error: Bash script not found.\n"; + return; } - if (!std::filesystem::exists(bashScriptPath)) { - message = "Could not find that script file."; - return false; - } + cout << "Enter output Dockerfile name (e.g., Dockerfile or Dockerfile_app): "; + cin >> outputFile; - if (outputFile.empty()) { - outputFile = "Dockerfile"; - } + if (outputFile.empty()) outputFile = "Dockerfile"; - std::ifstream scriptFile(bashScriptPath); - std::ofstream dockerfile(outputFile); + ifstream scriptFile(bashScriptPath); + ofstream dockerfile(outputFile); if (!scriptFile.is_open() || !dockerfile.is_open()) { - message = "Could not open one of the files."; - return false; + cout << "Error: Unable to open file(s).\n"; + return; } + // Write Dockerfile header dockerfile << "FROM " << baseImage << "\n"; dockerfile << "WORKDIR /app\n\n"; dockerfile << "# Auto-generated by Tux-Dock\n"; - std::string line; - while (std::getline(scriptFile, line)) { - if (line.empty()) { - continue; - } + string line; + while (getline(scriptFile, line)) { + if (line.empty()) continue; - if (line.rfind("#", 0) == 0 || line.rfind("#!", 0) == 0) { + // Skip comments or shebang + if (line.rfind("#", 0) == 0 || line.rfind("#!", 0) == 0) continue; - } dockerfile << "RUN " << line << "\n"; } @@ -305,1008 +361,70 @@ bool DockerManager::createDockerfile(const std::string& baseImage, dockerfile.close(); scriptFile.close(); + cout << "Dockerfile created successfully: " << outputFile << "\n"; + cout << "Enter image name to build from this Dockerfile (e.g., myimage): "; + cin >> imageName; + if (imageName.empty()) { - message = "Dockerfile created. Build skipped because no image name was provided."; - return true; - } - - const bool ok = runCommand("docker build -t " + shellEscape(imageName) + " -f " + - shellEscape(outputFile) + " ."); - message = ok ? "Dockerfile created and image build completed." : "Dockerfile created, but the image build failed."; - return ok; -} - -class TuxDockApp { -public: - void Run(); - -private: - enum class ModalMode { None, Input, Confirm, Select, Message }; - - struct RunContainerContext { - std::string image; - int port_count = 0; - std::vector ports; - }; - - struct MySqlContext { - std::string port; - std::string password; - std::string version; - }; - - struct DockerfileContext { - std::string base_image; - std::string script_path; - std::string output_file; - std::string image_name; - }; - - DockerManager docker_; - - std::vector menu_entries_ = { - "Pull Docker Image", - "Run/Create Interactive Container", - "List All Containers", - "List All Images", - "Start Container Interactively (boot new session)", - "Start Detached Container Session", - "Delete Docker Image", - "Stop Container", - "Remove Container", - "Attach Shell to Running Container", - "Run Detached Command in Container", - "Spin Up MySQL Container", - "Get Container IP Address", - "Create Dockerfile & Build Image from Bash Script", - "About Tux-Dock", - "Exit", - }; - - int menu_selected_ = 0; - std::string status_ = "Ready. Select an action and press Enter."; - - ModalMode modal_mode_ = ModalMode::None; - std::string modal_title_; - std::string modal_text_; - std::string modal_input_; - std::vector modal_select_entries_; - int modal_select_index_ = 0; - - ftxui::Component menu_component_ = ftxui::Menu(&menu_entries_, &menu_selected_); - ftxui::Component input_component_ = ftxui::Input(&modal_input_, "Type here"); - ftxui::Component select_component_ = ftxui::Menu(&modal_select_entries_, &modal_select_index_); - - std::function input_callback_; - std::function confirm_callback_; - std::function select_callback_; - - ftxui::ScreenInteractive* screen_ = nullptr; - - static bool IsDigits(const std::string& value); - static std::string ShortId(const std::string& id); - static bool IsValidPortMapping(const std::string& mapping); - - std::string FormatContainerList(const std::vector& containers) const; - std::string FormatImageList(const std::vector>& images) const; - - void SetStatus(const std::string& message); - void OpenInput(const std::string& title, - const std::string& text, - std::function callback, - bool secret = false); - void OpenConfirm(const std::string& title, const std::string& text, std::function callback); - void OpenSelect(const std::string& title, - const std::string& text, - std::vector options, - std::function callback); - void OpenMessage(const std::string& title, const std::string& text); - void ResolveInput(bool confirmed); - void ResolveConfirm(bool confirmed); - void ResolveSelect(bool confirmed); - void CloseMessage(); - - void ExecuteSelectedAction(); - void ActionPullImage(); - void ActionRunContainer(); - void ActionListContainers(); - void ActionListImages(); - void ActionStartInteractive(); - void ActionStartDetached(); - void ActionDeleteImage(); - void ActionStopContainer(); - void ActionRemoveContainer(); - void ActionExecShell(); - void ActionExecDetachedCommand(); - void ActionSpinUpMySQL(); - void ActionShowContainerIP(); - void ActionCreateDockerfile(); - void ActionAbout(); - void PromptPortCountAndRun(const std::shared_ptr& context); - void PromptNextPort(const std::shared_ptr& context, int index); - - void PromptContainerSelection(const std::string& title, - std::function callback); - void PromptImageSelection(const std::string& title, - std::function callback); - void RunDeferredStatusAction(const std::string& wait_message, - std::function action); - static void ClearTerminal(); - void RunWithRestoredIO(const std::function& action, - bool clear_before = false, - bool clear_after = false); - - bool OnEvent(ftxui::Event event); - ftxui::Element Render() const; - ftxui::Element RenderModal() const; -}; - -bool TuxDockApp::IsDigits(const std::string& value) { - if (value.empty()) { - return false; - } - for (unsigned char c : value) { - if (!std::isdigit(c)) { - return false; - } - } - return true; -} - -std::string TuxDockApp::ShortId(const std::string& id) { - if (id.size() <= 12) { - return id; - } - return id.substr(0, 12); -} - -bool TuxDockApp::IsValidPortMapping(const std::string& mapping) { - const std::size_t separator = mapping.find(':'); - if (separator == std::string::npos) { - return false; - } - const std::string host = mapping.substr(0, separator); - const std::string container = mapping.substr(separator + 1); - return IsDigits(host) && IsDigits(container); -} - -std::string TuxDockApp::FormatContainerList( - const std::vector& containers) const { - std::stringstream ss; - for (std::size_t i = 0; i < containers.size(); ++i) { - const auto& container = containers[i]; - const std::string state = container.running ? "running" : "stopped"; - const std::string ports = container.ports.empty() ? "none" : container.ports; - ss << i + 1 << ". " << container.name << " (" << ShortId(container.id) << ")" - << " [" << state << "]" - << " ports: " << ports; - if (i + 1 < containers.size()) { - ss << "\n"; - } - } - return ss.str(); -} - -std::string TuxDockApp::FormatImageList(const std::vector>& images) const { - std::stringstream ss; - for (std::size_t i = 0; i < images.size(); ++i) { - ss << i + 1 << ". " << images[i].second << " (" << ShortId(images[i].first) << ")"; - if (i + 1 < images.size()) { - ss << "\n"; - } - } - return ss.str(); -} - -void TuxDockApp::SetStatus(const std::string& message) { - status_ = message; -} - -void TuxDockApp::OpenInput(const std::string& title, - const std::string& text, - std::function callback, - bool secret) { - modal_mode_ = ModalMode::Input; - modal_title_ = title; - modal_text_ = text; - modal_input_.clear(); - ftxui::InputOption input_option; - input_option.password = secret; - input_component_ = ftxui::Input(&modal_input_, "Type here", input_option); - input_callback_ = std::move(callback); -} - -void TuxDockApp::OpenConfirm(const std::string& title, - const std::string& text, - std::function callback) { - modal_mode_ = ModalMode::Confirm; - modal_title_ = title; - modal_text_ = text; - confirm_callback_ = std::move(callback); -} - -void TuxDockApp::OpenSelect(const std::string& title, - const std::string& text, - std::vector options, - std::function callback) { - modal_mode_ = ModalMode::Select; - modal_title_ = title; - modal_text_ = text; - modal_select_entries_ = std::move(options); - modal_select_index_ = 0; - select_component_ = ftxui::Menu(&modal_select_entries_, &modal_select_index_); - select_callback_ = std::move(callback); -} - -void TuxDockApp::OpenMessage(const std::string& title, const std::string& text) { - modal_mode_ = ModalMode::Message; - modal_title_ = title; - modal_text_ = text; -} - -void TuxDockApp::ResolveInput(bool confirmed) { - const std::string value = modal_input_; - auto callback = std::move(input_callback_); - - modal_mode_ = ModalMode::None; - modal_title_.clear(); - modal_text_.clear(); - modal_input_.clear(); - input_component_ = ftxui::Input(&modal_input_, "Type here"); - input_callback_ = {}; - - if (callback) { - callback(confirmed, value); - } -} - -void TuxDockApp::ResolveConfirm(bool confirmed) { - auto callback = std::move(confirm_callback_); - - modal_mode_ = ModalMode::None; - modal_title_.clear(); - modal_text_.clear(); - confirm_callback_ = {}; - - if (callback) { - callback(confirmed); - } -} - -void TuxDockApp::ResolveSelect(bool confirmed) { - const int selected = modal_select_index_; - auto callback = std::move(select_callback_); - - modal_mode_ = ModalMode::None; - modal_title_.clear(); - modal_text_.clear(); - modal_select_entries_.clear(); - modal_select_index_ = 0; - select_component_ = ftxui::Menu(&modal_select_entries_, &modal_select_index_); - select_callback_ = {}; - - if (callback) { - callback(confirmed, selected); - } -} - -void TuxDockApp::CloseMessage() { - modal_mode_ = ModalMode::None; - modal_title_.clear(); - modal_text_.clear(); -} - -void TuxDockApp::PromptContainerSelection( - const std::string& title, - std::function callback) { - auto containers = docker_.getContainerList(); - if (containers.empty()) { - SetStatus("No containers available."); + cout << "No image name provided. Skipping build.\n"; return; } - std::vector options; - options.reserve(containers.size()); - for (const auto& container : containers) { - const std::string state = container.running ? "running" : "stopped"; - const std::string ports = container.ports.empty() ? "none" : container.ports; - options.push_back(container.name + " (" + ShortId(container.id) + ") [" + state + "] ports: " + ports); - } - - OpenSelect(title, - "Select a container with arrows and press Enter.", - std::move(options), - [this, containers, callback = std::move(callback)](bool confirmed, int selected) mutable { - if (!confirmed) { - SetStatus("Action cancelled."); - return; - } - if (selected < 0 || selected >= static_cast(containers.size())) { - SetStatus("Please choose a valid container."); - return; - } - const auto& chosen = containers[static_cast(selected)]; - callback(chosen.id, chosen.name); - }); + cout << "Building Docker image '" << imageName << "'...\n"; + runCommand("docker build -t " + imageName + " -f " + outputFile + " ."); + cout << "Docker build command executed.\n"; } -void TuxDockApp::PromptImageSelection(const std::string& title, - std::function callback) { - auto images = docker_.getImageList(); - if (images.empty()) { - SetStatus("No images found."); - return; - } - - std::vector options; - options.reserve(images.size()); - for (const auto& image : images) { - options.push_back(image.second + " (" + ShortId(image.first) + ")"); - } - - OpenSelect(title, - "Select an image with arrows and press Enter.", - std::move(options), - [this, images, callback = std::move(callback)](bool confirmed, int selected) mutable { - if (!confirmed) { - SetStatus("Action cancelled."); - return; - } - if (selected < 0 || selected >= static_cast(images.size())) { - SetStatus("Please choose a valid image."); - return; - } - const auto& chosen = images[static_cast(selected)]; - callback(chosen.first, chosen.second); - }); -} - -void TuxDockApp::ClearTerminal() { - std::cout << "\x1b[2J\x1b[H" << std::flush; -} - -void TuxDockApp::RunWithRestoredIO(const std::function& action, - bool clear_before, - bool clear_after) { - if (screen_ != nullptr) { - screen_->WithRestoredIO([&] { - if (clear_before) { - ClearTerminal(); - } - action(); - if (clear_after) { - ClearTerminal(); - } - })(); - return; - } - if (clear_before) { - ClearTerminal(); - } - action(); - if (clear_after) { - ClearTerminal(); - } -} - -void TuxDockApp::RunDeferredStatusAction(const std::string& wait_message, - std::function action) { - SetStatus(wait_message); - - if (screen_ == nullptr) { - SetStatus(action()); - return; - } - - ftxui::ScreenInteractive* active_screen = screen_; - active_screen->PostEvent(ftxui::Event::Custom); - - std::thread([this, active_screen, action = std::move(action)]() mutable { - const std::string final_message = action(); - if (ftxui::ScreenInteractive::Active() != active_screen) { - return; - } - active_screen->Post([this, final_message] { SetStatus(final_message); }); - active_screen->PostEvent(ftxui::Event::Custom); - }).detach(); -} - -void TuxDockApp::ActionPullImage() { - const std::vector quick_images = { - "debian:stable", - "ubuntu:noble", - "rockylinux:9.3", - "alpine:latest", - }; - - std::vector options = quick_images; - options.push_back("Custom image..."); - - OpenSelect("Pull Docker Image", - "Select an image with arrows, then press Enter.", - std::move(options), - [this, quick_images](bool confirmed, int selected) { - if (!confirmed) { - SetStatus("Image pull cancelled."); - return; - } - - if (selected < 0 || selected > static_cast(quick_images.size())) { - SetStatus("Please select a valid image option."); - return; - } - - if (selected < static_cast(quick_images.size())) { - const std::string image = quick_images[static_cast(selected)]; - RunDeferredStatusAction("Please wait, pulling image...", [this, image] { - std::string message; - docker_.pullImage(image, message); - return message; - }); - return; - } - - OpenInput("Pull Docker Image", - "Enter custom Docker image name:", - [this](bool custom_confirmed, const std::string& image) { - if (!custom_confirmed) { - SetStatus("Image pull cancelled."); - return; - } - if (image.empty()) { - SetStatus("Image name cannot be empty."); - ActionPullImage(); - return; - } - RunDeferredStatusAction("Please wait, pulling image...", [this, image] { - std::string message; - docker_.pullImage(image, message); - return message; - }); - }); - }); -} - -void TuxDockApp::ActionRunContainer() { - auto images = docker_.getImageList(); - std::vector options; - options.reserve(images.size() + 1); - for (const auto& image : images) { - options.push_back(image.second + " (" + ShortId(image.first) + ")"); - } - options.push_back("Custom image..."); - - OpenSelect("Run Interactive Container", - "Select an image with arrows, then press Enter.", - std::move(options), - [this, images](bool confirmed, int selected) { - if (!confirmed) { - SetStatus("Container run cancelled."); - return; - } - - if (selected < 0 || selected > static_cast(images.size())) { - SetStatus("Please select a valid image option."); - return; - } - - auto context = std::make_shared(); - if (selected == static_cast(images.size())) { - OpenInput("Run Interactive Container", - "Enter custom image name:", - [this, context](bool custom_confirmed, const std::string& image_name) { - if (!custom_confirmed) { - SetStatus("Container run cancelled."); - return; - } - if (image_name.empty()) { - SetStatus("Image name cannot be empty."); - ActionRunContainer(); - return; - } - context->image = image_name; - PromptPortCountAndRun(context); - }); - return; - } - - context->image = images[static_cast(selected)].second; - PromptPortCountAndRun(context); - }); -} - -void TuxDockApp::PromptPortCountAndRun(const std::shared_ptr& context) { - OpenInput("Port Mappings", - "How many port mappings? (0 for none)", - [this, context](bool confirmed, const std::string& count_value) { - if (!confirmed) { - SetStatus("Container run cancelled."); - return; - } - if (!IsDigits(count_value)) { - SetStatus("Please enter a valid number."); - PromptPortCountAndRun(context); - return; - } - - context->port_count = std::stoi(count_value); - context->ports.clear(); - if (context->port_count < 0) { - SetStatus("Port mappings cannot be negative."); - PromptPortCountAndRun(context); - return; - } - - PromptNextPort(context, 0); - }); -} - -void TuxDockApp::PromptNextPort(const std::shared_ptr& context, int index) { - if (index >= context->port_count) { - std::string message; - SetStatus("Starting interactive container..."); - RunWithRestoredIO([this, context, &message] { - docker_.runContainerInteractive(context->image, context->ports, message); - }, true, true); - SetStatus(message); - return; - } - - OpenInput("Port Mapping", - "Enter mapping #" + std::to_string(index + 1) + " (example: 8080:80)", - [this, context, index](bool confirmed, const std::string& mapping) { - if (!confirmed) { - SetStatus("Container run cancelled."); - return; - } - if (!IsValidPortMapping(mapping)) { - SetStatus("Use host:container format, for example 8080:80."); - PromptNextPort(context, index); - return; - } - - context->ports.push_back(mapping); - PromptNextPort(context, index + 1); - }); -} - -void TuxDockApp::ActionListContainers() { - auto containers = docker_.getContainerList(); - if (containers.empty()) { - SetStatus("No containers available."); - return; - } - OpenMessage("Containers", FormatContainerList(containers)); - SetStatus("Container list opened."); -} - -void TuxDockApp::ActionListImages() { - auto images = docker_.getImageList(); - if (images.empty()) { - SetStatus("No images found."); - return; - } - OpenMessage("Images", FormatImageList(images)); - SetStatus("Image list opened."); -} - -void TuxDockApp::ActionStartInteractive() { - PromptContainerSelection("Start Interactively", [this](const std::string& id, const std::string&) { - std::string message; - SetStatus("Starting interactive session..."); - RunWithRestoredIO([this, &id, &message] { docker_.startInteractive(id, message); }, true, true); - SetStatus(message); - }); -} - -void TuxDockApp::ActionStartDetached() { - PromptContainerSelection("Start Detached", [this](const std::string& id, const std::string&) { - std::string message; - SetStatus("Starting container in detached mode..."); - RunWithRestoredIO([this, &id, &message] { docker_.startDetached(id, message); }); - SetStatus(message); - }); -} - -void TuxDockApp::ActionDeleteImage() { - PromptImageSelection("Delete Image", [this](const std::string& id, const std::string& tag) { - OpenConfirm("Delete Image", - "Delete image " + tag + "?", - [this, id](bool confirmed) { - if (!confirmed) { - SetStatus("Image deletion cancelled."); - return; - } - RunDeferredStatusAction("Please wait, deleting image...", [this, id] { - std::string message; - docker_.deleteImage(id, message); - return message; - }); - }); - }); -} - -void TuxDockApp::ActionStopContainer() { - PromptContainerSelection("Stop Container", [this](const std::string& id, const std::string&) { - RunDeferredStatusAction("Please wait, stopping container...", [this, id] { - std::string message; - docker_.stopContainer(id, message); - return message; - }); - }); -} - -void TuxDockApp::ActionRemoveContainer() { - PromptContainerSelection("Remove Container", [this](const std::string& id, const std::string& name) { - OpenConfirm("Remove Container", - "Remove container " + name + "?", - [this, id](bool confirmed) { - if (!confirmed) { - SetStatus("Container removal cancelled."); - return; - } - RunDeferredStatusAction("Please wait, removing container...", [this, id] { - std::string message; - docker_.removeContainer(id, message); - return message; - }); - }); - }); -} - -void TuxDockApp::ActionExecShell() { - PromptContainerSelection("Open Shell", [this](const std::string& id, const std::string&) { - std::string message; - SetStatus("Opening shell session..."); - RunWithRestoredIO([this, &id, &message] { docker_.execShell(id, message); }, true, true); - SetStatus(message); - }); -} - -void TuxDockApp::ActionExecDetachedCommand() { - PromptContainerSelection("Run Detached Command", - [this](const std::string& id, const std::string& name) { - OpenInput("Detached Command", - "Enter command to run in " + name + ":", - [this, id](bool confirmed, const std::string& command) { - if (!confirmed) { - SetStatus("Detached command cancelled."); - return; - } - std::string message; - SetStatus("Running detached command..."); - RunWithRestoredIO([this, &id, &command, &message] { - docker_.execDetachedCommand(id, command, message); - }); - SetStatus(message); - }); - }); -} - -void TuxDockApp::ActionSpinUpMySQL() { - auto context = std::make_shared(); - - OpenInput("MySQL Setup", - "Enter port mapping (example: 3306:3306)", - [this, context](bool confirmed, const std::string& port) { - if (!confirmed) { - SetStatus("MySQL setup cancelled."); - return; - } - if (!IsValidPortMapping(port)) { - SetStatus("Use host:container format, for example 3306:3306."); - return; - } - context->port = port; - - OpenInput("MySQL Setup", - "Enter MySQL root password:", - [this, context](bool pwd_confirmed, const std::string& password) { - if (!pwd_confirmed) { - SetStatus("MySQL setup cancelled."); - return; - } - if (password.empty()) { - SetStatus("Password cannot be empty."); - return; - } - context->password = password; - - OpenInput("MySQL Setup", - "Enter MySQL version tag (example: 8)", - [this, context](bool ver_confirmed, const std::string& version) { - if (!ver_confirmed) { - SetStatus("MySQL setup cancelled."); - return; - } - if (version.empty()) { - SetStatus("Version tag cannot be empty."); - return; - } - context->version = version; - - std::string message; - SetStatus("Launching MySQL container..."); - RunWithRestoredIO([this, context, &message] { - docker_.spinUpMySQL(context->port, - context->password, - context->version, - message); - }); - SetStatus(message); - }); - }, - true); - }); -} - -void TuxDockApp::ActionShowContainerIP() { - PromptContainerSelection("Container IP", [this](const std::string& id, const std::string&) { - std::string message; - SetStatus("Looking up container IP..."); - RunWithRestoredIO([this, &id, &message] { docker_.showContainerIP(id, message); }); - SetStatus(message); - }); -} - -void TuxDockApp::ActionCreateDockerfile() { - auto context = std::make_shared(); - - OpenInput("Dockerfile Builder", - "Enter base image (example: ubuntu:22.04)", - [this, context](bool base_confirmed, const std::string& base) { - if (!base_confirmed) { - SetStatus("Dockerfile workflow cancelled."); - return; - } - if (base.empty()) { - SetStatus("Base image cannot be empty."); - return; - } - context->base_image = base; - - OpenInput("Dockerfile Builder", - "Enter bash script path:", - [this, context](bool script_confirmed, const std::string& script_path) { - if (!script_confirmed) { - SetStatus("Dockerfile workflow cancelled."); - return; - } - if (script_path.empty()) { - SetStatus("Script path cannot be empty."); - return; - } - context->script_path = script_path; - - OpenInput("Dockerfile Builder", - "Enter output Dockerfile name (leave empty for Dockerfile)", - [this, context](bool output_confirmed, const std::string& output_file) { - if (!output_confirmed) { - SetStatus("Dockerfile workflow cancelled."); - return; - } - context->output_file = output_file; - - OpenInput("Dockerfile Builder", - "Enter image name to build (leave empty to skip build)", - [this, context](bool image_confirmed, - const std::string& image_name) { - if (!image_confirmed) { - SetStatus("Dockerfile workflow cancelled."); - return; - } - context->image_name = image_name; - - std::string message; - SetStatus("Creating Dockerfile and running build..."); - RunWithRestoredIO([this, context, &message] { - docker_.createDockerfile(context->base_image, - context->script_path, - context->output_file, - context->image_name, - message); - }); - SetStatus(message); - }); - }); - }); - }); -} - -void TuxDockApp::ActionAbout() { - SetStatus("Tux-Dock 022526-dev\n" - "Created by markmental\n\n" - "GitHub:\n" - "https://github.com/MARKMENTAL/tuxdock\n\n" - "Forgejo:\n" - "https://mentalnet.xyz/forgejo/markmental/tuxdock"); -} - -void TuxDockApp::ExecuteSelectedAction() { - switch (menu_selected_) { - case 0: - ActionPullImage(); - break; - case 1: - ActionRunContainer(); - break; - case 2: - ActionListContainers(); - break; - case 3: - ActionListImages(); - break; - case 4: - ActionStartInteractive(); - break; - case 5: - ActionStartDetached(); - break; - case 6: - ActionDeleteImage(); - break; - case 7: - ActionStopContainer(); - break; - case 8: - ActionRemoveContainer(); - break; - case 9: - ActionExecShell(); - break; - case 10: - ActionExecDetachedCommand(); - break; - case 11: - ActionSpinUpMySQL(); - break; - case 12: - ActionShowContainerIP(); - break; - case 13: - ActionCreateDockerfile(); - break; - case 14: - ActionAbout(); - break; - case 15: - SetStatus("Exiting Tux-Dock."); - if (screen_ != nullptr) { - screen_->ExitLoopClosure()(); - } - break; - default: - SetStatus("Please choose a valid menu option."); - break; - } -} - -bool TuxDockApp::OnEvent(ftxui::Event event) { - if (modal_mode_ == ModalMode::Input) { - if (event == ftxui::Event::Return) { - ResolveInput(true); - return true; - } - if (event == ftxui::Event::Escape) { - ResolveInput(false); - return true; - } - return input_component_->OnEvent(event); - } - - if (modal_mode_ == ModalMode::Confirm) { - if (event == ftxui::Event::Character("y") || event == ftxui::Event::Character("Y") || - event == ftxui::Event::Return) { - ResolveConfirm(true); - return true; - } - if (event == ftxui::Event::Character("n") || event == ftxui::Event::Character("N") || - event == ftxui::Event::Escape) { - ResolveConfirm(false); - return true; - } - return true; - } - - if (modal_mode_ == ModalMode::Select) { - if (event == ftxui::Event::Return) { - ResolveSelect(true); - return true; - } - if (event == ftxui::Event::Escape) { - ResolveSelect(false); - return true; - } - return select_component_->OnEvent(event); - } - - if (modal_mode_ == ModalMode::Message) { - if (event == ftxui::Event::Return || event == ftxui::Event::Escape) { - CloseMessage(); - return true; - } - return true; - } - - if (event == ftxui::Event::Return) { - ExecuteSelectedAction(); - return true; - } - - return false; -} - -ftxui::Element TuxDockApp::RenderModal() const { - using namespace ftxui; - - Element body; - Element footer; - - if (modal_mode_ == ModalMode::Input) { - body = vbox(ftxui::Elements{paragraph(modal_text_), separator(), input_component_->Render() | border}); - footer = text("Enter: confirm Esc: cancel") | dim; - } else if (modal_mode_ == ModalMode::Confirm) { - body = paragraph(modal_text_); - footer = text("Y/Enter: confirm N/Esc: cancel") | dim; - } else if (modal_mode_ == ModalMode::Select) { - body = vbox(ftxui::Elements{paragraph(modal_text_), separator(), select_component_->Render() | frame | vscroll_indicator}); - footer = text("Up/Down: choose Enter: confirm Esc: cancel") | dim; - } else { - body = paragraph(modal_text_) | vscroll_indicator | frame | size(HEIGHT, LESS_THAN, 14); - footer = text("Enter/Esc: close") | dim; - } - - return window(text(modal_title_), vbox(ftxui::Elements{body, separator(), footer})) | - size(WIDTH, GREATER_THAN, 70) | size(HEIGHT, GREATER_THAN, 12) | center; -} - -ftxui::Element TuxDockApp::Render() const { - using namespace ftxui; - - Elements status_lines; - { - std::stringstream ss(status_); - std::string line; - while (std::getline(ss, line)) { - status_lines.push_back(line.empty() ? text(" ") : text(line)); - } - if (status_lines.empty()) { - status_lines.push_back(text(" ")); - } - } - - auto actions_panel = window(text("Actions"), - vbox(ftxui::Elements{menu_component_->Render() | frame | vscroll_indicator, - separator(), - text("Up/Down: navigate Enter: select") | dim})) | - size(WIDTH, GREATER_THAN, 48) | flex; - - auto status_panel = window(text("Status"), vbox(ftxui::Elements{vbox(std::move(status_lines)) | yflex | frame | vscroll_indicator, - separator(), - text("High-level updates only") | dim})) | - size(WIDTH, GREATER_THAN, 48) | flex; - - Element base = hbox(ftxui::Elements{actions_panel, separator(), status_panel}) | border; - - if (modal_mode_ == ModalMode::None) { - return base; - } - - return dbox({base, RenderModal() | clear_under | center}); -} - -void TuxDockApp::Run() { - auto root = ftxui::Renderer(menu_component_, [this] { return Render(); }); - auto app = ftxui::CatchEvent(root, [this](ftxui::Event event) { return OnEvent(event); }); - - auto screen = ftxui::ScreenInteractive::TerminalOutput(); - screen_ = &screen; - screen.Loop(app); - screen_ = nullptr; -} +// ---------------- Menu ---------------- int main() { - TuxDockApp app; - app.Run(); - return 0; + DockerManager docker; + int option; + + while (true) { + cout << "\nTux-Dock: Docker Management Menu\n" + << "----------------------------------\n" + << "1. Pull Docker Image\n" + << "2. Run/Create Interactive Container\n" + << "3. List All Containers\n" + << "4. List All Images\n" + << "5. Start Container Interactively (boot new session)\n" + << "6. Start Detached Container Session\n" + << "7. Delete Docker Image\n" + << "8. Stop Container\n" + << "9. Remove Container\n" + << "10. Attach Shell to Running Container\n" + << "11. Run Detached Command in Container\n" + << "12. Spin Up MySQL Container\n" + << "13. Get Container IP Address\n" + << "14. Create Dockerfile & Build Image from Bash Script\n" + << "15. Exit\n" + << "----------------------------------\n" + << "Choose an option: "; + + cin >> option; + + switch (option) { + case 1: docker.pullImage(); break; + case 2: docker.runContainerInteractive(); break; + case 3: docker.listContainers(); break; + case 4: docker.listImages(); break; + case 5: docker.startInteractive(); break; + case 6: docker.startDetached(); break; + case 7: docker.deleteImage(); break; + case 8: docker.stopContainer(); break; + case 9: docker.removeContainer(); break; + case 10: docker.execShell(); break; + case 11: docker.execDetachedCommand(); break; + case 12: docker.spinUpMySQL(); break; + case 13: docker.showContainerIP(); break; + case 14: docker.createDockerfile(); break; + case 15: + cout << "Exiting Tux-Dock.\n"; + return 0; + default: + cout << "Invalid option.\n"; + } + } } +