0.1 update, make the stop container code more robust and fix some bugs, update the about screen for version 0.1
This commit is contained in:
parent
91fa50370a
commit
e8e93d7b21
17 changed files with 449 additions and 43 deletions
85
main.cpp
85
main.cpp
|
|
@ -1,4 +1,5 @@
|
|||
#include "src/docker_manager.hpp"
|
||||
#include "src/operation_state.hpp"
|
||||
|
||||
#include <cctype>
|
||||
#include <filesystem>
|
||||
|
|
@ -21,7 +22,7 @@ public:
|
|||
int Run();
|
||||
|
||||
private:
|
||||
enum class ModalMode { None, Input, Confirm, Select, Message };
|
||||
enum class ModalMode { None, Input, Confirm, Select, Message, Busy };
|
||||
struct RunContainerContext { std::string image; int port_count = 0; std::vector<std::string> 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; };
|
||||
|
|
@ -46,6 +47,8 @@ private:
|
|||
ftxui::ScreenInteractive* screen_ = nullptr;
|
||||
std::thread refresh_thread_;
|
||||
std::vector<std::thread> action_threads_;
|
||||
OperationState operation_state_;
|
||||
std::size_t spinner_frame_ = 0;
|
||||
|
||||
static bool IsDigits(const std::string& value);
|
||||
static std::string ShortId(const std::string& id);
|
||||
|
|
@ -68,6 +71,8 @@ private:
|
|||
void PromptContainerSelection(const std::string&, std::function<void(const std::string&, const std::string&)>);
|
||||
void PromptImageSelection(const std::string&, std::function<void(const std::string&, const std::string&)>);
|
||||
void RunDeferredStatusAction(const std::string&, std::function<std::string()>);
|
||||
void BeginBusyOperation(const std::string&, const std::string&, std::function<std::string()>);
|
||||
void BeginStopOperation(const std::string& id);
|
||||
void RefreshState(const std::string& message = "Refreshing Docker state...");
|
||||
void ApplyRefreshResults(DockerManager::ListResult<DockerManager::ContainerInfo> containers,
|
||||
DockerManager::ListResult<DockerManager::ImageInfo> images);
|
||||
|
|
@ -155,16 +160,49 @@ void TuxDockApp::PromptContainerSelection(const std::string& title, std::functio
|
|||
void TuxDockApp::PromptImageSelection(const std::string& title, std::function<void(const std::string&, const std::string&)> callback) { if (images_.empty()) { RefreshState("No cached images. Refreshing..."); return; } std::vector<std::string> options; 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, callback = std::move(callback)](bool ok, int selected) { if (!ok) return SetStatus("Action cancelled."); if (selected < 0 || selected >= static_cast<int>(images_.size())) return SetStatus("Please choose a valid image."); const auto& image = images_[static_cast<std::size_t>(selected)]; callback(image.first, image.second); }); }
|
||||
|
||||
void TuxDockApp::RunDeferredStatusAction(const std::string& wait, std::function<std::string()> action) {
|
||||
SetStatus(wait);
|
||||
BeginBusyOperation("Working", wait, std::move(action));
|
||||
}
|
||||
|
||||
void TuxDockApp::BeginBusyOperation(const std::string& title,
|
||||
const std::string& message,
|
||||
std::function<std::string()> action) {
|
||||
operation_state_.begin(title, message);
|
||||
modal_mode_ = ModalMode::Busy;
|
||||
spinner_frame_ = 0;
|
||||
auto* active = screen_;
|
||||
if (active) active->PostEvent(ftxui::Event::Custom);
|
||||
action_threads_.emplace_back([this, active, action = std::move(action)]() mutable {
|
||||
const auto message = action();
|
||||
if (active && ftxui::ScreenInteractive::Active() == active) {
|
||||
active->Post([this, message] { SetStatus(message); RefreshState(); });
|
||||
active->Post([this, message] {
|
||||
operation_state_.complete(message);
|
||||
modal_mode_ = ModalMode::None;
|
||||
OpenMessage("Operation complete", message);
|
||||
RefreshState();
|
||||
});
|
||||
active->PostEvent(ftxui::Event::Custom);
|
||||
}
|
||||
});
|
||||
}
|
||||
void TuxDockApp::BeginStopOperation(const std::string& id) {
|
||||
operation_state_.begin("Stopping container", "Stopping and refreshing state...");
|
||||
modal_mode_ = ModalMode::Busy;
|
||||
auto* active = screen_;
|
||||
action_threads_.emplace_back([this, active, id] {
|
||||
std::string message;
|
||||
const bool stopped = docker_.stopContainer(id, message);
|
||||
auto containers = docker_.getContainerList();
|
||||
auto images = docker_.getImageList();
|
||||
if (!active || ftxui::ScreenInteractive::Active() != active) return;
|
||||
active->Post([this, stopped, message, containers = std::move(containers), images = std::move(images)]() mutable {
|
||||
ApplyRefreshResults(std::move(containers), std::move(images));
|
||||
operation_state_.complete(message);
|
||||
modal_mode_ = ModalMode::None;
|
||||
OpenMessage(stopped ? "Container stopped" : "Stop failed", message);
|
||||
});
|
||||
active->PostEvent(ftxui::Event::Custom);
|
||||
});
|
||||
}
|
||||
void TuxDockApp::ClearTerminal() { std::cout << "\x1b[2J\x1b[H" << std::flush; }
|
||||
void TuxDockApp::RunWithRestoredIO(const std::function<void()>& action, bool before, bool after) { if (screen_) screen_->WithRestoredIO([&] { if (before) ClearTerminal(); action(); if (after) ClearTerminal(); })(); else action(); }
|
||||
|
||||
|
|
@ -183,18 +221,18 @@ void TuxDockApp::ActionListImages() {
|
|||
}
|
||||
OpenListMessage("Images", FormatImageList(images_));
|
||||
}
|
||||
void TuxDockApp::ActionPullImage() { OpenInput("Pull Docker Image", "Enter image name:", [this](bool ok, const std::string& image) { if (!ok) return; RunDeferredStatusAction("Please wait, pulling image...", [this, image] { std::string m; docker_.pullImage(image, m); return m; }); }); }
|
||||
void TuxDockApp::ActionPullImage() { OpenInput("Pull Docker Image", "Enter image name:", [this](bool ok, const std::string& image) { if (!ok) return; BeginBusyOperation("Pulling image", "Please wait...", [this, image] { std::string m; docker_.pullImage(image, m); return m; }); }); }
|
||||
void TuxDockApp::ActionRunContainer() { PromptImageSelection("Run Interactive Container", [this](const std::string&, const std::string& tag) { auto context = std::make_shared<RunContainerContext>(); context->image = tag; PromptPortCountAndRun(context); }); }
|
||||
void TuxDockApp::PromptPortCountAndRun(const std::shared_ptr<RunContainerContext>& c) { OpenInput("Port Mappings", "How many port mappings? (0 for none)", [this, c](bool ok, const std::string& value) { if (!ok) return; if (!IsDigits(value)) return SetStatus("Please enter a valid number."); c->port_count = std::stoi(value); PromptNextPort(c, 0); }); }
|
||||
void TuxDockApp::PromptNextPort(const std::shared_ptr<RunContainerContext>& c, int index) { if (index >= c->port_count) { std::string m; RunWithRestoredIO([this, c, &m] { docker_.runContainerInteractive(c->image, c->ports, m); }, true, true); SetStatus(m); RefreshState(); return; } OpenInput("Port Mapping", "Enter mapping #" + std::to_string(index + 1), [this, c, index](bool ok, const std::string& value) { if (!ok) return; if (!IsValidPortMapping(value)) return SetStatus("Use host:container format."); c->ports.push_back(value); PromptNextPort(c, index + 1); }); }
|
||||
void TuxDockApp::ActionStartInteractive() { PromptContainerSelection("Start Interactively", [this](const std::string& id, const std::string&) { std::string m; RunWithRestoredIO([this, &id, &m] { docker_.startInteractive(id, m); }, true, true); SetStatus(m); RefreshState(); }); }
|
||||
void TuxDockApp::ActionStartDetached() { PromptContainerSelection("Start Detached", [this](const std::string& id, const std::string&) { RunDeferredStatusAction("Starting container...", [this, id] { std::string m; docker_.startDetached(id, m); return m; }); }); }
|
||||
void TuxDockApp::ActionDeleteImage() { PromptImageSelection("Delete Image", [this](const std::string& id, const std::string& tag) { OpenConfirm("Delete Image", "Delete image " + tag + "?", [this, id](bool ok) { if (ok) RunDeferredStatusAction("Deleting image...", [this, id] { std::string m; docker_.deleteImage(id, m); return m; }); }); }); }
|
||||
void TuxDockApp::ActionStopContainer() { PromptContainerSelection("Stop Container", [this](const std::string& id, const std::string&) { RunDeferredStatusAction("Stopping container...", [this, id] { std::string m; docker_.stopContainer(id, m); return m; }); }); }
|
||||
void TuxDockApp::ActionRemoveContainer() { PromptContainerSelection("Remove Container", [this](const std::string& id, const std::string& name) { OpenConfirm("Remove Container", "Remove container " + name + "?", [this, id](bool ok) { if (ok) RunDeferredStatusAction("Removing container...", [this, id] { std::string m; docker_.removeContainer(id, m); return m; }); }); }); }
|
||||
void TuxDockApp::ActionStartDetached() { PromptContainerSelection("Start Detached", [this](const std::string& id, const std::string&) { BeginBusyOperation("Starting container", "Please wait...", [this, id] { std::string m; docker_.startDetached(id, m); return m; }); }); }
|
||||
void TuxDockApp::ActionDeleteImage() { PromptImageSelection("Delete Image", [this](const std::string& id, const std::string& tag) { OpenConfirm("Delete Image", "Delete image " + tag + "?", [this, id](bool ok) { if (ok) BeginBusyOperation("Deleting image", "Please wait...", [this, id] { std::string m; docker_.deleteImage(id, m); return m; }); }); }); }
|
||||
void TuxDockApp::ActionStopContainer() { PromptContainerSelection("Stop Container", [this](const std::string& id, const std::string&) { BeginStopOperation(id); }); }
|
||||
void TuxDockApp::ActionRemoveContainer() { PromptContainerSelection("Remove Container", [this](const std::string& id, const std::string& name) { OpenConfirm("Remove Container", "Remove container " + name + "?", [this, id](bool ok) { if (ok) BeginBusyOperation("Removing container", "Please wait...", [this, id] { std::string m; docker_.removeContainer(id, m); return m; }); }); }); }
|
||||
void TuxDockApp::ActionExecShell() { PromptContainerSelection("Open Shell", [this](const std::string& id, const std::string&) { std::string m; RunWithRestoredIO([this, &id, &m] { docker_.execShell(id, m); }, true, true); SetStatus(m); }); }
|
||||
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 ok, const std::string& command) { if (!ok) return; RunDeferredStatusAction("Running command...", [this, id, command] { std::string m; docker_.execDetachedCommand(id, command, m); return m; }); }); }); }
|
||||
void TuxDockApp::ActionSpinUpMySQL() { OpenInput("MySQL Setup", "Enter port mapping:", [this](bool ok, const std::string& port) { if (!ok || !IsValidPortMapping(port)) return SetStatus("Use host:container format."); OpenInput("MySQL Setup", "Enter root password:", [this, port](bool ok2, const std::string& password) { if (!ok2) return; OpenInput("MySQL Setup", "Enter version tag:", [this, port, password](bool ok3, const std::string& version) { if (!ok3) return; RunDeferredStatusAction("Launching MySQL...", [this, port, password, version] { std::string m; docker_.spinUpMySQL(port, password, version, m); return m; }); }); }, true); }); }
|
||||
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 ok, const std::string& command) { if (!ok) return; BeginBusyOperation("Running command", "Please wait...", [this, id, command] { std::string m; docker_.execDetachedCommand(id, command, m); return m; }); }); }); }
|
||||
void TuxDockApp::ActionSpinUpMySQL() { OpenInput("MySQL Setup", "Enter port mapping:", [this](bool ok, const std::string& port) { if (!ok || !IsValidPortMapping(port)) return SetStatus("Use host:container format."); OpenInput("MySQL Setup", "Enter root password:", [this, port](bool ok2, const std::string& password) { if (!ok2) return; OpenInput("MySQL Setup", "Enter version tag:", [this, port, password](bool ok3, const std::string& version) { if (!ok3) return; BeginBusyOperation("Launching MySQL", "Please wait...", [this, port, password, version] { std::string m; docker_.spinUpMySQL(port, password, version, m); return m; }); }); }, true); }); }
|
||||
void TuxDockApp::ActionCreateDockerfile() {
|
||||
OpenInput("Dockerfile Builder", "Enter base image:", [this](bool ok, const std::string& base) {
|
||||
if (!ok) return;
|
||||
|
|
@ -204,7 +242,7 @@ void TuxDockApp::ActionCreateDockerfile() {
|
|||
if (!ok3) return;
|
||||
OpenInput("Dockerfile Builder", "Enter image name (empty skips build):", [this, base, script, output](bool ok4, const std::string& image) {
|
||||
if (!ok4) return;
|
||||
RunDeferredStatusAction("Creating Dockerfile...", [this, base, script, output, image] {
|
||||
BeginBusyOperation("Building image", "Creating Dockerfile and running build...", [this, base, script, output, image] {
|
||||
std::string m;
|
||||
docker_.createDockerfile(base, script, output, image, m);
|
||||
return m;
|
||||
|
|
@ -214,11 +252,19 @@ void TuxDockApp::ActionCreateDockerfile() {
|
|||
});
|
||||
});
|
||||
}
|
||||
void TuxDockApp::ActionAbout() { SetStatus("Tux-Dock 022526-dev\nCreated by markmental"); }
|
||||
void TuxDockApp::ActionAbout() { OpenMessage("About Tux-Dock", "Tux-Dock 0.1-beta | Created by markmental"); }
|
||||
|
||||
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: ActionCreateDockerfile(); break; case 13: ActionAbout(); break; case 14: if (screen_) screen_->ExitLoopClosure()(); break; default: 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::Return || event == ftxui::Event::Character("y")) { ResolveConfirm(true); return true; } if (event == ftxui::Event::Escape || event == ftxui::Event::Character("n")) { 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; }
|
||||
bool TuxDockApp::OnEvent(ftxui::Event event) {
|
||||
if (modal_mode_ == ModalMode::Busy) return true;
|
||||
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::Return || event == ftxui::Event::Character("y")) { ResolveConfirm(true); return true; } if (event == ftxui::Event::Escape || event == ftxui::Event::Character("n")) { 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;
|
||||
|
|
@ -232,17 +278,26 @@ ftxui::Element TuxDockApp::RenderModal() const {
|
|||
} else if (modal_mode_ == ModalMode::Select) {
|
||||
body = vbox(Elements{paragraph(modal_text_), separator(), select_component_->Render() | frame | vscroll_indicator});
|
||||
footer = text("Up/Down: choose Enter: confirm Esc: cancel") | dim;
|
||||
} else if (modal_mode_ == ModalMode::Busy) {
|
||||
static const std::string spinner = "|/-\\";
|
||||
body = vbox(Elements{
|
||||
text(operation_state_.message()),
|
||||
separator(),
|
||||
text(std::string(" ") + spinner[spinner_frame_ % spinner.size()]) | bold,
|
||||
});
|
||||
footer = text("Please wait; input is disabled") | dim;
|
||||
} else {
|
||||
body = (modal_content_ ? std::move(modal_content_) : paragraph(modal_text_)) |
|
||||
frame | vscroll_indicator |
|
||||
size(HEIGHT, LESS_THAN, 18) | size(WIDTH, LESS_THAN, 96);
|
||||
footer = text("Enter/Esc: close") | dim;
|
||||
}
|
||||
return window(text(modal_title_), vbox(Elements{body, separator(), footer})) |
|
||||
const std::string title = modal_mode_ == ModalMode::Busy ? operation_state_.title() : modal_title_;
|
||||
return window(text(title), vbox(Elements{body, separator(), footer})) |
|
||||
size(WIDTH, LESS_THAN, 96) | size(WIDTH, GREATER_THAN, 42) |
|
||||
size(HEIGHT, LESS_THAN, 24) | size(HEIGHT, GREATER_THAN, 10) | center;
|
||||
}
|
||||
ftxui::Element TuxDockApp::Render() const { using namespace ftxui; Elements lines; std::stringstream s(status_); std::string line; while (std::getline(s, line)) lines.push_back(line.empty() ? text(" ") : text(line)); auto base = hbox(Elements{window(text("Actions"), vbox(Elements{menu_component_->Render() | frame | vscroll_indicator, separator(), text("Up/Down: navigate Enter: select") | dim})) | size(WIDTH, GREATER_THAN, 48) | flex, separator(), window(text("Status"), vbox(Elements{vbox(std::move(lines)) | yflex | frame | vscroll_indicator, separator(), text("Engine API cache") | dim})) | size(WIDTH, GREATER_THAN, 48) | flex}) | border; return modal_mode_ == ModalMode::None ? base : dbox({base, RenderModal() | clear_under | center}); }
|
||||
ftxui::Element TuxDockApp::Render() const { using namespace ftxui; auto base = window(text("Actions"), vbox(Elements{menu_component_->Render() | frame | vscroll_indicator, separator(), text("Up/Down: navigate Enter: select") | dim})) | size(WIDTH, GREATER_THAN, 56) | size(WIDTH, LESS_THAN, 90) | center; return modal_mode_ == ModalMode::None ? base : dbox({base, RenderModal() | clear_under | center}); }
|
||||
int TuxDockApp::Run() {
|
||||
std::string connection_error;
|
||||
if (!docker_.checkConnection(connection_error)) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue