Compare commits

...

2 commits

Author SHA1 Message Date
665809a21a Code cleanup, finish of changes for 0.1.1
Some checks failed
Build & Upload tux-dock / build (push) Has been cancelled
2026-08-15 18:12:21 -04:00
9e2660e524 README cleanup, make compile script more robust 2026-08-15 17:06:30 -04:00
7 changed files with 588 additions and 146 deletions

View file

@ -1,6 +1,6 @@
# Tux-Dock Development Log
## 0.1-beta
## 0.1.1-beta
This release establishes the first beta-quality Docker integration and TUI workflow.

View file

@ -14,8 +14,6 @@ It gives you a guided, keyboard-first TUI for common Docker operations without m
- Rich container display with state and forwarded ports.
- Interactive shell handoff with clean terminal clear before/after shell transitions.
- Image operations: pull/list/delete with curated quick picks and custom image support.
- Script-to-image workflow: generate Dockerfile from bash script, then optionally build.
- MySQL quick start flow with version/password/port prompts.
- Docker Engine API access through `/var/run/docker.sock` for structured list and lifecycle operations.
- Direct `fork`/`exec` process execution for CLI-backed streaming and interactive commands.
- Persistent container listings that retain exited containers.
@ -55,7 +53,7 @@ sudo ./build/tux-dock
./compile.sh --web-test-view 9000
```
The web report is generated under `/tmp` and includes exact CTest output, test summaries, Docker integration output, and a text rendition of the TUI flow. It requires `nc` or `netcat`; press `Ctrl-C` to stop the server. The default port is `8095`; pass a port after `--web-test-view` to override it. `--web-test-view --no-test` is invalid. Normal test runs include a Docker integration test using `debian:forky`, so Docker must be available.
The web report is generated under `/tmp` and includes exact CTest output, test summaries, Docker integration output, and a text rendition of the TUI flow. It requires `nc`, `netcat`, or Nmap's `ncat`; press `Ctrl-C` to stop the server. The default port is `8095`; pass a port after `--web-test-view` to override it. `--web-test-view --no-test` is invalid. Normal test runs include a Docker integration test using `debian:forky`, so Docker must be available. On systems with 1 GB or less of available memory, the script automatically uses a smaller build configuration and a single build job.
---
@ -64,18 +62,17 @@ The web report is generated under `/tmp` and includes exact CTest output, test s
Current TUI actions:
1. Pull Docker Image
2. Run/Create Interactive Container
2. Create Container
3. List All Containers
4. List All Images
5. Start Container Interactively (boot new session)
6. Start Detached Container Session
7. Delete Docker Image
8. Stop Container
9. Remove Container
10. Attach Shell to Running Container
11. Run Detached Command in Container
12. About Tux-Dock
13. Exit
5. Start Detached Container Session
6. Delete Docker Image
7. Stop Container
8. Remove Container
9. Attach Shell to Running Container
10. Run Detached Command in Container
11. About Tux-Dock
12. Exit
---
@ -83,47 +80,6 @@ Current TUI actions:
Tux-Dock is organized around the TUI, Docker manager, Engine API client, and direct process runner:
```cpp
class DockerManager {
public:
struct ContainerInfo {
std::string id;
std::string name;
std::string status;
std::string ports;
bool running;
};
bool checkConnection(...);
ListResult<ContainerInfo> getContainerList() const;
ListResult<ImageInfo> getImageList() const;
bool pullImage(...);
bool runContainerInteractive(...);
bool startInteractive(...);
bool startDetached(...);
bool stopContainer(...);
bool removeContainer(...);
bool deleteImage(...);
bool execShell(...);
bool execDetachedCommand(...);
};
class TuxDockApp {
public:
void Run();
private:
// TUI orchestration layer with modal busy states
void OpenInput(...);
void OpenSelect(...);
void OpenConfirm(...);
void BeginBusyOperation(...);
void RunWithRestoredIO(...);
void ExecuteSelectedAction();
// action handlers bridge UI -> DockerManager
};
```
### Responsibilities
- `DockerEngineClient`
@ -153,7 +109,7 @@ This split keeps Docker behavior isolated while making UI behavior easier to ext
```bash
cmake -S . -B build -DBUILD_TESTING=ON
cmake --build build -j
cmake --build build
ctest --test-dir build --output-on-failure
```
@ -163,7 +119,7 @@ ctest --test-dir build --output-on-failure
## About / Version
- Version: `0.1-beta`
- Version: `0.1.1-beta`
- Created by: `markmental`
- GitHub: https://github.com/MARKMENTAL/tuxdock
- Forgejo: https://mentalnet.xyz/forgejo-v2/markmental/tuxdock

View file

@ -6,6 +6,32 @@ run_tests=1
web_view=0
web_port=8095
memory_limit=$(cat /sys/fs/cgroup/memory.max 2>/dev/null || true)
if [ -z "$memory_limit" ] || [ "$memory_limit" = max ]; then
memory_limit=$(cat /sys/fs/cgroup/memory/memory.limit_in_bytes 2>/dev/null || true)
fi
if [ -z "$memory_limit" ] || [ "$memory_limit" = max ] || [ "$memory_limit" -ge 9223372036854771712 ] 2>/dev/null; then
memory_limit=$(awk '/MemTotal:/ { print $2 * 1024; exit }' /proc/meminfo 2>/dev/null || true)
fi
low_ram=0
if [ -n "$memory_limit" ] && [ "$memory_limit" -le 1073741824 ] 2>/dev/null; then
low_ram=1
fi
build_jobs=4
cpu_count=$(getconf _NPROCESSORS_ONLN 2>/dev/null || true)
case "$cpu_count" in
''|*[!0-9]*) ;;
*)
if [ "$cpu_count" -lt "$build_jobs" ]; then
build_jobs=$cpu_count
fi
;;
esac
if [ "$low_ram" -eq 1 ]; then
build_jobs=1
fi
while [ "$#" -gt 0 ]; do
argument=$1
shift
@ -35,13 +61,20 @@ if [ "$web_view" -eq 1 ] && [ "$run_tests" -eq 0 ]; then
exit 2
fi
if [ "$run_tests" -eq 1 ]; then
cmake -S . -B build -DBUILD_TESTING=ON
else
cmake -S . -B build -DBUILD_TESTING=OFF
cmake_args=""
if [ "$low_ram" -eq 1 ]; then
cmake_args="-DCMAKE_BUILD_TYPE=MinSizeRel"
fi
cmake --build build -j
if [ "$run_tests" -eq 1 ]; then
# shellcheck disable=SC2086
cmake -S . -B build -DBUILD_TESTING=ON $cmake_args
else
# shellcheck disable=SC2086
cmake -S . -B build -DBUILD_TESTING=OFF $cmake_args
fi
CMAKE_BUILD_PARALLEL_LEVEL="$build_jobs" cmake --build build
if [ "$run_tests" -eq 1 ]; then
report_dir=$(mktemp -d /tmp/tux-dock-test.XXXXXX)
@ -146,27 +179,40 @@ if [ "$run_tests" -eq 1 ]; then
cat "$report"
} >"$response"
if command -v nc >/dev/null 2>&1; then
if command -v ncat >/dev/null 2>&1; then
nc_command=ncat
nc_args='-l'
nc_address='0.0.0.0'
nc_port="$web_port"
elif command -v nc >/dev/null 2>&1; then
nc_command=nc
if "$nc_command" -h 2>&1 | grep -q -- '-N'; then
nc_args='-l -N -s 0.0.0.0 -p'
else
nc_args='-l -s 0.0.0.0 -p'
fi
nc_address=''
nc_port="$web_port"
elif command -v netcat >/dev/null 2>&1; then
nc_command=netcat
if "$nc_command" -h 2>&1 | grep -q -- '-N'; then
nc_args='-l -N -s 0.0.0.0 -p'
else
nc_args='-l -s 0.0.0.0 -p'
fi
nc_address=''
nc_port="$web_port"
else
printf '%s\n' "Web report generated at $report, but nc/netcat is unavailable." >&2
exit "$test_status"
fi
if "$nc_command" -h 2>&1 | grep -q -- '-N'; then
nc_args='-l -N -s 0.0.0.0 -p'
else
nc_args='-l -s 0.0.0.0 -p'
fi
printf '%s\n' "Test report: http://0.0.0.0:$web_port/"
printf '%s\n' "Report file: $report"
printf '%s\n' "Press Ctrl-C to stop the server."
while :; do
# shellcheck disable=SC2086
"$nc_command" $nc_args "$web_port" <"$response"
"$nc_command" $nc_args ${nc_address:+"$nc_address"} "$nc_port" <"$response"
done
fi
exit "$test_status"

553
main.cpp
View file

@ -25,17 +25,36 @@ public:
private:
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 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", "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", "About Tux-Dock", "Exit"};
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_, modal_text_, modal_input_;
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;
@ -54,24 +73,43 @@ private:
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 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 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 ResolveInput(bool);
void ResolveConfirm(bool);
void ResolveSelect(bool);
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 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 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();
@ -81,13 +119,39 @@ private:
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;
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; }
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 p = mapping.find(':'); return p != std::string::npos && IsDigits(mapping.substr(0, p)) && IsDigits(mapping.substr(p + 1)); }
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;
@ -150,18 +214,139 @@ void TuxDockApp::ApplyRefreshResults(
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::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();
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& c : containers_) options.push_back(c.name + " (" + ShortId(c.id) + ") [" + (c.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& c = containers_[static_cast<std::size_t>(selected)]; callback(c.id, c.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); }); }
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));
@ -232,7 +417,20 @@ void TuxDockApp::BeginStopOperation(const std::string& id) {
});
}
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(); }
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()) {
@ -249,31 +447,242 @@ 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; 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&) { 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; BeginBusyOperation("Running command", "Please wait...", [this, id, command] { std::string m; docker_.execDetachedCommand(id, command, m); return m; }); }); }); }
void TuxDockApp::ActionAbout() { OpenMessage("About Tux-Dock", "Tux-Dock 0.1-beta | Created by markmental"); }
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::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: ActionAbout(); break; case 12: if (screen_) screen_->ExitLoopClosure()(); break; default: break; } }
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 mapping #" + std::to_string(index + 1),
[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.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: 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; } return true; }
if (event == ftxui::Event::Return) { ExecuteSelectedAction(); 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 {
@ -281,13 +690,21 @@ ftxui::Element TuxDockApp::RenderModal() const {
Element body;
Element footer;
if (modal_mode_ == ModalMode::Input) {
body = vbox(Elements{paragraph(modal_text_), separator(), input_component_->Render() | border});
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});
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 = "|/-\\";
@ -303,12 +720,30 @@ ftxui::Element TuxDockApp::RenderModal() const {
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_;
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}); }
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)) {
@ -332,14 +767,22 @@ int TuxDockApp::Run() {
}
auto root = ftxui::Renderer(menu_component_, [this] { return Render(); });
auto app = ftxui::CatchEvent(root, [this](ftxui::Event e) { return OnEvent(e); });
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();
for (auto& thread : action_threads_) {
if (thread.joinable()) thread.join();
}
return 0;
}
int main() { TuxDockApp app; return app.Run(); }
int main() {
TuxDockApp app;
return app.Run();
}

View file

@ -89,27 +89,22 @@ bool DockerManager::pullImage(const std::string& image, std::string& message) co
return ok;
}
bool DockerManager::runContainerInteractive(const std::string& image,
const std::vector<std::string>& ports,
std::string& message) const {
if (image.empty()) {
message = "Please choose an image first.";
bool DockerManager::createContainer(const std::string& name,
const std::string& image,
const std::vector<std::string>& ports,
std::string& message) const {
if (name.empty() || image.empty()) {
message = "Please provide a container name and choose an image first.";
return false;
}
std::vector<std::string> args{"docker", "run", "-it"};
std::vector<std::string> args{"docker", "create", "--name", name};
for (const auto& port : ports) {
args.push_back("-p");
args.push_back(port);
}
args.insert(args.end(), {image, "/bin/sh"});
const bool ok = runProcess(args, message, true);
if (ok) message = "Interactive container session finished.";
return ok;
}
bool DockerManager::startInteractive(const std::string& id, std::string& message) const {
const bool ok = runProcess({"docker", "start", "-ai", id}, message, true);
if (ok) message = "Interactive container session finished.";
args.insert(args.end(), {image, "sleep", "infinity"});
const bool ok = runProcess(args, message);
if (ok) message = "Container created and ready to start when you are ready.";
return ok;
}

View file

@ -32,10 +32,10 @@ public:
ListResult<ImageInfo> getImageList() const;
bool pullImage(const std::string& image, std::string& message) const;
bool runContainerInteractive(const std::string& image,
const std::vector<std::string>& ports,
std::string& message) const;
bool startInteractive(const std::string& containerId, std::string& message) const;
bool createContainer(const std::string& name,
const std::string& image,
const std::vector<std::string>& ports,
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;

View file

@ -33,9 +33,9 @@ cleanup() {
}
trap cleanup EXIT INT TERM
printf '%s\n' "[TUI] Actions > Run/Create Interactive Container"
printf '%s\n' "[TUI] Actions > Create Container"
printf '%s\n' "[TUI] Busy |"
printf '%s\n' "[TUI] docker run --name $name $image sh -c '<tests>'"
printf '%s\n' "[TUI] docker create --name $name $image sleep infinity"
if ! docker image inspect "$image" >/dev/null 2>&1; then
printf '%s\n' "[TUI] Pull Docker Image: $image"
@ -53,8 +53,10 @@ run_in_container() {
printf '%s\n' "[TUI] Complete: $command_name succeeded"
}
run_raw "docker run -d --name $name $image sleep 300" docker run -d --name "$name" "$image" sleep 300
run_raw "docker create --name $name $image sleep infinity" docker create --name "$name" "$image" sleep infinity
printf '%s\n' "[TUI] Complete: Container created"
run_raw "docker start $name" docker start "$name"
printf '%s\n' "[TUI] Complete: Container started"
run_in_container "ls /" ls /
run_in_container "cat /etc/os-release" cat /etc/os-release
run_in_container "apt-get update" apt-get update