tuxdock/main.cpp

882 lines
31 KiB
C++

#include "src/docker_manager.hpp"
#include "src/operation_state.hpp"
#include <cctype>
#include <csignal>
#include <atomic>
#include <chrono>
#include <filesystem>
#include <fstream>
#include <functional>
#include <iostream>
#include <memory>
#include <sstream>
#include <string>
#include <thread>
#include <utility>
#include <vector>
#include <ftxui/component/component.hpp>
#include <ftxui/component/screen_interactive.hpp>
#include <ftxui/dom/elements.hpp>
namespace {
volatile std::sig_atomic_t g_sigint_busy = 0;
volatile std::sig_atomic_t g_sigint_trapped = 0;
volatile std::sig_atomic_t g_sigtstp_trapped = 0;
bool g_sigint_trap_installed = false;
bool g_sigtstp_trap_installed = false;
struct sigaction g_sigint_previous {};
struct sigaction g_sigtstp_previous {};
void TuxDockHandleSigint(int signal_number) {
if (g_sigint_busy != 0) {
++g_sigint_trapped;
return;
}
if (g_sigint_trap_installed) {
const auto previous_handler = g_sigint_previous.sa_handler;
if (previous_handler != nullptr && previous_handler != SIG_DFL) {
if (previous_handler != SIG_IGN) previous_handler(signal_number);
return;
}
}
std::signal(signal_number, SIG_DFL);
std::raise(signal_number);
}
void TuxDockHandleSigtstp(int signal_number) {
if (g_sigint_busy != 0) {
++g_sigtstp_trapped;
return;
}
if (g_sigtstp_trap_installed) {
const auto previous_handler = g_sigtstp_previous.sa_handler;
if (previous_handler != nullptr && previous_handler != SIG_DFL) {
if (previous_handler != SIG_IGN) previous_handler(signal_number);
return;
}
}
std::signal(signal_number, SIG_DFL);
std::raise(signal_number);
}
void InstallSignalTraps() {
g_sigint_trapped = 0;
g_sigtstp_trapped = 0;
g_sigint_busy = 1;
if (!g_sigint_trap_installed) {
struct sigaction trap {};
sigemptyset(&trap.sa_mask);
trap.sa_flags = SA_RESTART;
trap.sa_handler = TuxDockHandleSigint;
if (sigaction(SIGINT, &trap, &g_sigint_previous) == 0) {
g_sigint_trap_installed = true;
}
}
if (!g_sigtstp_trap_installed) {
struct sigaction trap {};
sigemptyset(&trap.sa_mask);
trap.sa_flags = SA_RESTART;
trap.sa_handler = TuxDockHandleSigtstp;
if (sigaction(SIGTSTP, &trap, &g_sigtstp_previous) == 0) {
g_sigtstp_trap_installed = true;
}
}
}
void RemoveSignalTraps() {
g_sigint_busy = 0;
g_sigint_trapped = 0;
g_sigtstp_trapped = 0;
if (g_sigint_trap_installed) {
sigaction(SIGINT, &g_sigint_previous, nullptr);
g_sigint_trap_installed = false;
}
if (g_sigtstp_trap_installed) {
sigaction(SIGTSTP, &g_sigtstp_previous, nullptr);
g_sigtstp_trap_installed = false;
}
}
}
class TuxDockApp {
public:
int Run();
private:
enum class ModalMode { None, Input, Confirm, Select, Message, Busy };
struct RunContainerContext {
std::string name;
std::string image;
int port_count = 0;
std::vector<std::string> ports;
};
DockerManager docker_;
std::vector<DockerManager::ContainerInfo> containers_;
std::vector<DockerManager::ImageInfo> images_;
std::vector<std::string> menu_entries_ = {
"Pull Docker Image",
"Create Container",
"List All Containers",
"List All Images",
"Start Detached Container Session",
"Delete Docker Image",
"Stop Container",
"Remove Container",
"Attach Shell to Running Container",
"Run Detached Command in Container",
"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_;
ftxui::Element modal_content_;
std::vector<std::string> 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<void(bool, const std::string&)> input_callback_;
std::function<void(bool)> confirm_callback_;
std::function<void(bool, int)> select_callback_;
ftxui::ScreenInteractive* screen_ = nullptr;
std::thread refresh_thread_;
std::thread spinner_thread_;
std::atomic<bool> spinner_stop_{true};
std::vector<std::thread> action_threads_;
OperationState operation_state_;
std::size_t spinner_frame_ = 0;
static bool IsDigits(const std::string& value);
static bool IsValidContainerName(const std::string& name);
static std::string ShortId(const std::string& id);
static bool IsValidPortMapping(const std::string& mapping);
ftxui::Element FormatContainerList(const std::vector<DockerManager::ContainerInfo>& containers) const;
ftxui::Element FormatImageList(const std::vector<DockerManager::ImageInfo>& images) const;
void SetStatus(const std::string& message) { status_ = message; }
void OpenInput(const std::string&, const std::string&,
std::function<void(bool, const std::string&)>, bool secret = false);
void OpenConfirm(const std::string&, const std::string&, std::function<void(bool)>);
void OpenSelect(const std::string&, const std::string&, std::vector<std::string>,
std::function<void(bool, int)>);
void OpenMessage(const std::string& title, const std::string& text);
void OpenListMessage(const std::string& title, ftxui::Element content);
void ResolveInput(bool);
void ResolveConfirm(bool);
void ResolveSelect(bool);
void CloseMessage();
void ExecuteSelectedAction();
void ActionPullImage();
void ActionRunContainer();
void ActionListContainers();
void ActionListImages();
void ActionStartDetached();
void ActionDeleteImage();
void ActionStopContainer();
void ActionRemoveContainer();
void ActionExecShell();
void ActionExecDetachedCommand();
void ActionAbout();
void PromptPortCountAndRun(const std::shared_ptr<RunContainerContext>&);
void PromptNextPort(const std::shared_ptr<RunContainerContext>&, int);
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 StartSpinner();
void StopSpinner();
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);
static void ClearTerminal();
void RunWithRestoredIO(const std::function<void()>&, bool clear_before = false,
bool clear_after = false);
bool OnEvent(ftxui::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;
}
bool TuxDockApp::IsValidContainerName(const std::string& name) {
if (name.empty()) return false;
for (unsigned char c : name) {
if (!(std::isalnum(c) || c == '_' || c == '.' || c == '-')) return false;
}
return true;
}
std::string TuxDockApp::ShortId(const std::string& id) {
return id.size() <= 12 ? id : id.substr(0, 12);
}
bool TuxDockApp::IsValidPortMapping(const std::string& mapping) {
const auto separator = mapping.find(':');
return separator != std::string::npos &&
IsDigits(mapping.substr(0, separator)) &&
IsDigits(mapping.substr(separator + 1));
}
ftxui::Element TuxDockApp::FormatContainerList(const std::vector<DockerManager::ContainerInfo>& items) const {
using namespace ftxui;
Elements rows;
for (std::size_t i = 0; i < items.size(); ++i) {
const auto& container = items[i];
rows.push_back(vbox(Elements{
text("[" + std::to_string(i + 1) + "] " + container.name) | bold,
text((container.running ? "RUNNING" : "STOPPED") + std::string(" id ") + ShortId(container.id)) | dim,
text("ports: " + (container.ports.empty() ? std::string("none") : container.ports)) | dim,
}));
if (i + 1 < items.size()) rows.push_back(separator());
}
return vbox(std::move(rows));
}
ftxui::Element TuxDockApp::FormatImageList(const std::vector<DockerManager::ImageInfo>& items) const {
using namespace ftxui;
Elements rows;
for (std::size_t i = 0; i < items.size(); ++i) {
const auto& image = items[i];
rows.push_back(vbox(Elements{
text("[" + std::to_string(i + 1) + "] " + image.second) | bold,
text("id " + ShortId(image.first)) | dim,
}));
if (i + 1 < items.size()) rows.push_back(separator());
}
return vbox(std::move(rows));
}
void TuxDockApp::RefreshState(const std::string& message) {
SetStatus(message);
if (refresh_thread_.joinable()) refresh_thread_.join();
auto* active = screen_;
refresh_thread_ = std::thread([this, active] {
auto containers = docker_.getContainerList();
auto images = docker_.getImageList();
if (active == nullptr || ftxui::ScreenInteractive::Active() != active) return;
active->Post([this, containers = std::move(containers), images = std::move(images)]() mutable {
ApplyRefreshResults(std::move(containers), std::move(images));
});
active->PostEvent(ftxui::Event::Custom);
});
}
void TuxDockApp::ApplyRefreshResults(
DockerManager::ListResult<DockerManager::ContainerInfo> containers,
DockerManager::ListResult<DockerManager::ImageInfo> images) {
std::string error;
if (containers.ok()) {
containers_ = std::move(containers.items);
} else {
error = "Containers: " + containers.error;
}
if (images.ok()) {
images_ = std::move(images.items);
} else {
if (!error.empty()) error += "\n";
error += "Images: " + images.error;
}
SetStatus(error.empty() ? "Docker state refreshed." : "Refresh failed; cached state preserved.\n" + error);
}
void TuxDockApp::OpenInput(const std::string& title,
const std::string& text,
std::function<void(bool, const std::string&)> callback,
bool secret) {
modal_mode_ = ModalMode::Input;
modal_title_ = title;
modal_text_ = text;
modal_input_.clear();
ftxui::InputOption option;
option.password = secret;
input_component_ = ftxui::Input(&modal_input_, "Type here", option);
input_callback_ = std::move(callback);
}
void TuxDockApp::OpenConfirm(const std::string& title,
const std::string& text,
std::function<void(bool)> 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<std::string> options,
std::function<void(bool, int)> 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;
modal_content_ = {};
}
void TuxDockApp::OpenListMessage(const std::string& title, ftxui::Element content) {
modal_mode_ = ModalMode::Message;
modal_title_ = title;
modal_text_.clear();
modal_content_ = std::move(content);
}
void TuxDockApp::ResolveInput(bool confirmed) {
auto callback = std::move(input_callback_);
const auto value = modal_input_;
modal_mode_ = ModalMode::None;
input_callback_ = {};
if (callback) callback(confirmed, value);
}
void TuxDockApp::ResolveConfirm(bool confirmed) {
auto callback = std::move(confirm_callback_);
modal_mode_ = ModalMode::None;
confirm_callback_ = {};
if (callback) callback(confirmed);
}
void TuxDockApp::ResolveSelect(bool confirmed) {
auto callback = std::move(select_callback_);
const int selected = modal_select_index_;
modal_mode_ = ModalMode::None;
select_callback_ = {};
if (callback) callback(confirmed, selected);
}
void TuxDockApp::CloseMessage() {
modal_mode_ = ModalMode::None;
modal_content_ = {};
}
void TuxDockApp::PromptContainerSelection(
const std::string& title,
std::function<void(const std::string&, const std::string&)> callback) {
if (containers_.empty()) {
RefreshState("No cached containers. Refreshing...");
return;
}
std::vector<std::string> options;
for (const auto& container : containers_) {
options.push_back(container.name + " (" + ShortId(container.id) + ") [" +
(container.running ? "running" : "stopped") + "]");
}
OpenSelect(
title,
"Select a container 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>(containers_.size())) {
return SetStatus("Please choose a valid container.");
}
const auto& container = containers_[static_cast<std::size_t>(selected)];
callback(container.id, container.name);
});
}
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) {
BeginBusyOperation("Working", wait, std::move(action));
}
void TuxDockApp::StartSpinner() {
StopSpinner();
spinner_stop_ = false;
auto* active = screen_;
spinner_thread_ = std::thread([this, active] {
while (active && !spinner_stop_.load() && ftxui::ScreenInteractive::Active() == active) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
if (spinner_stop_.load() || ftxui::ScreenInteractive::Active() != active) break;
active->RequestAnimationFrame();
active->Post([this] { if (!spinner_stop_.load()) ++spinner_frame_; });
}
});
}
void TuxDockApp::StopSpinner() {
spinner_stop_ = true;
if (spinner_thread_.joinable()) spinner_thread_.join();
}
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;
InstallSignalTraps();
auto* active = screen_;
StartSpinner();
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] {
RemoveSignalTraps();
operation_state_.complete(message);
StopSpinner();
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;
InstallSignalTraps();
auto* active = screen_;
spinner_frame_ = 0;
StartSpinner();
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 {
RemoveSignalTraps();
ApplyRefreshResults(std::move(containers), std::move(images));
operation_state_.complete(message);
StopSpinner();
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_) {
action();
return;
}
screen_->WithRestoredIO([&] {
if (before) ClearTerminal();
action();
if (after) ClearTerminal();
})();
}
void TuxDockApp::ActionListContainers() {
if (containers_.empty()) {
OpenMessage("Containers", "No containers found.");
return;
}
OpenListMessage("Containers", FormatContainerList(containers_));
}
void TuxDockApp::ActionListImages() {
if (images_.empty()) {
OpenMessage("Images", "No images found.");
return;
}
OpenListMessage("Images", FormatImageList(images_));
}
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 message;
docker_.pullImage(image, message);
return message;
});
});
}
void TuxDockApp::ActionRunContainer() {
PromptImageSelection(
"Create Container",
[this](const std::string&, const std::string& tag) {
auto context = std::make_shared<RunContainerContext>();
context->image = tag;
OpenInput(
"Container Name", "Enter a name for the container:",
[this, context](bool ok, const std::string& name) {
if (!ok) return;
if (!IsValidContainerName(name)) {
return SetStatus(
"Use letters, numbers, '.', '_', or '-' in the container name.");
}
context->name = name;
PromptPortCountAndRun(context);
});
});
}
void TuxDockApp::PromptPortCountAndRun(
const std::shared_ptr<RunContainerContext>& context) {
OpenInput(
"Port Mappings", "How many port mappings? (0 for none)",
[this, context](bool ok, const std::string& value) {
if (!ok) return;
if (!IsDigits(value)) return SetStatus("Please enter a valid number.");
context->port_count = std::stoi(value);
PromptNextPort(context, 0);
});
}
void TuxDockApp::PromptNextPort(
const std::shared_ptr<RunContainerContext>& context,
int index) {
if (index >= context->port_count) {
BeginBusyOperation(
"Creating container", "Please wait...",
[this, context] {
std::string message;
docker_.createContainer(context->name, context->image,
context->ports, message);
return message;
});
return;
}
OpenInput(
"Port Mapping", "Enter host-to-container port mapping #" + std::to_string(index + 1) + ". e.g: 8080:80.",
[this, context, index](bool ok, const std::string& value) {
if (!ok) return;
if (!IsValidPortMapping(value)) {
return SetStatus("Use host:container format.");
}
context->ports.push_back(value);
PromptNextPort(context, index + 1);
});
}
void TuxDockApp::ActionStartDetached() {
PromptContainerSelection(
"Start Detached",
[this](const std::string& id, const std::string&) {
BeginBusyOperation(
"Starting container", "Please wait...",
[this, id] {
std::string message;
docker_.startDetached(id, message);
return 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 ok) {
if (!ok) return;
BeginBusyOperation(
"Deleting image", "Please wait...",
[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&) {
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) return;
BeginBusyOperation(
"Removing container", "Please wait...",
[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;
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 ok, const std::string& command) {
if (!ok) return;
BeginBusyOperation(
"Running command", "Please wait...",
[this, id, command] {
std::string message;
docker_.execDetachedCommand(id, command, message);
return message;
});
});
});
}
void TuxDockApp::ActionAbout() {
OpenMessage("About Tux-Dock", "Tux-Dock 0.1.2-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: ActionStartDetached(); break;
case 5: ActionDeleteImage(); break;
case 6: ActionStopContainer(); break;
case 7: ActionRemoveContainer(); break;
case 8: ActionExecShell(); break;
case 9: ActionExecDetachedCommand(); break;
case 10: ActionAbout(); break;
case 11:
if (screen_) screen_->ExitLoopClosure()();
break;
default: break;
}
}
bool TuxDockApp::OnEvent(ftxui::Event event) {
if (modal_mode_ == ModalMode::Busy) {
if (event == ftxui::Event::Custom) ++spinner_frame_;
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;
}
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(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(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 = "|/-\\";
Elements busy_elements = Elements{
text(operation_state_.message()),
separator(),
text(std::string(" ") + spinner[spinner_frame_ % spinner.size()]) | bold,
};
if (g_sigint_trapped > 0) {
busy_elements.push_back(text("Ctrl+C ignored: operation in progress") | dim);
}
if (g_sigtstp_trapped > 0) {
busy_elements.push_back(text("Ctrl+Z ignored: operation in progress") | dim);
}
body = vbox(std::move(busy_elements));
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;
}
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;
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)) {
std::cerr << "Unable to connect to Docker Engine.\n"
<< "Socket: /var/run/docker.sock\n"
<< "Reason: " << connection_error << "\n\n"
<< "Ensure Docker is running and your user can access the Docker socket.\n";
return 1;
}
const auto initial_containers = docker_.getContainerList();
const auto initial_images = docker_.getImageList();
if (initial_containers.ok()) containers_ = initial_containers.items;
if (initial_images.ok()) images_ = initial_images.items;
if (!initial_containers.ok() || !initial_images.ok()) {
SetStatus("Docker connected, but initial state could not be fully loaded.");
} else if (containers_.empty() && images_.empty()) {
SetStatus("Docker connected. No containers or images found.");
} else {
SetStatus("Docker connected. State loaded.");
}
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;
if (refresh_thread_.joinable()) refresh_thread_.join();
if (spinner_thread_.joinable()) spinner_thread_.join();
for (auto& thread : action_threads_) {
if (thread.joinable()) thread.join();
}
return 0;
}
int main() {
TuxDockApp app;
return app.Run();
}