Add a Frontend interface; make the process entry frontend-agnostic.

main.cpp was entirely TUI-specific (constructed Tui, parsed argv, drove
BootDispatch/DumpOutput/Run directly). Introduce a shared frontends layer so a
second frontend can reuse the whole launch flow:

  - src/frontends/frontend.hpp — abstract Frontend interface (BootDispatch,
    DumpCommandsMd, DumpOutput, Run), header-only, no GUI toolkit, no core dep.
  - src/frontends/frontend_main.{hpp,cpp} — frontend_main(argc, argv, Frontend&):
    all the argv parsing (--source/--restore/--batch/--commands-md/--help) and
    the boot → batch/run flow, driving any frontend through the interface.
  - Tui now implements Frontend (the four methods already matched; just marked
    override).
  - The TUI main.cpp shrinks to: construct Tui, call frontend_main. A second
    frontend's main() is identical with its own Frontend type.

Build: a small GUI-toolkit-free static lib essim_frontend (frontend_main.cpp)
is added at the top level when a frontend is selected, and the essim exe links
it. ESSIM_FRONTEND=none still builds core+tests only (no essim_frontend, no
FTXUI). Binary stays ./build/essim.

Behaviour unchanged across --batch/--commands-md/--help/exit codes; only the
usage text is genericised ("the TUI" → "the interface", "console screen" →
"console") now that the launcher is shared.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-03 20:34:29 +02:00
parent 0517a82a5c
commit af36f7c150
7 changed files with 162 additions and 99 deletions

View File

@@ -50,6 +50,12 @@ if(ESSIM_FRONTEND STREQUAL "none")
message(STATUS "essim: ESSIM_FRONTEND=none — core + tests only (no frontend, no GUI toolkit)")
elseif(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/src/frontends/${ESSIM_FRONTEND}/CMakeLists.txt")
message(STATUS "essim: building frontend '${ESSIM_FRONTEND}'")
# Shared, GUI-toolkit-free frontend support: the abstract Frontend interface
# (header-only) and the frontend-agnostic launcher frontend_main(). Every
# frontend's main() links this and forwards argv to it.
add_library(essim_frontend STATIC
"${CMAKE_CURRENT_SOURCE_DIR}/src/frontends/frontend_main.cpp")
target_include_directories(essim_frontend PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src)
add_subdirectory(src/frontends/${ESSIM_FRONTEND})
else()
message(FATAL_ERROR

View File

@@ -0,0 +1,30 @@
#ifndef _FRONTEND_HPP_
#define _FRONTEND_HPP_
#include <iosfwd>
#include <string>
// Abstract entry-point interface every frontend (TUI, GUI, …) implements, so
// one shared launcher (frontend_main) can drive any of them: parse argv, run
// boot commands, optionally dump output (batch / docs) and enter the event
// loop. Lives in the frontends layer — essim_core never depends on it.
class Frontend {
public:
virtual ~Frontend() = default;
// Dispatch one command synchronously, exactly as if the user typed it
// (e.g. "restore foo.essim" or "source bring-up.essim"), before the event
// loop starts — used to seed the system at boot.
virtual void BootDispatch(const std::string &raw) = 0;
// Write the command registry as Markdown (used for doc generation).
virtual void DumpCommandsMd(std::ostream &out) const = 0;
// Write the accumulated console output (batch mode: no event loop).
virtual void DumpOutput(std::ostream &out) const = 0;
// Enter the interactive event loop.
virtual void Run() = 0;
};
#endif // _FRONTEND_HPP_

View File

@@ -0,0 +1,99 @@
#include "frontends/frontend_main.hpp"
#include "frontends/frontend.hpp"
#include <fstream>
#include <iostream>
#include <string>
namespace {
void print_usage(const char *prog) {
std::cerr <<
"usage: " << prog << " [--batch] [--source FILE] [--restore FILE]\n"
" " << prog << " --commands-md [FILE]\n"
" " << prog << " --help\n"
" (no args) launch the interface on an empty system.\n"
" --source FILE after boot, run FILE as an essim script\n"
" (one command per line; same as the `source`\n"
" command). Output goes to the console.\n"
" --restore FILE after boot, restore the system snapshot in\n"
" FILE (same as the `restore` command).\n"
" Combine with --source to layer a script on\n"
" top of a restored snapshot.\n"
" --batch run --restore/--source, print the console\n"
" output to stdout, and exit without launching\n"
" the interface.\n"
" --commands-md [FILE] dump the command registry as Markdown.\n"
" With FILE: write there. Without: stdout.\n"
" (Used by `cmake --build build --target doc`.)\n"
" --help, -h show this help.\n";
}
} // namespace
int frontend_main(int argc, char **argv, Frontend &fe) {
std::string boot_restore;
std::string boot_source;
bool batch = false;
for (int i = 1; i < argc; ++i) {
std::string a = argv[i];
if (a == "--commands-md") {
if (i + 1 < argc) {
std::ofstream f(argv[++i]);
if (!f) {
std::cerr << "essim: cannot open " << argv[i] << " for writing\n";
return 1;
}
fe.DumpCommandsMd(f);
} else {
fe.DumpCommandsMd(std::cout);
}
return 0;
}
if (a == "--source") {
if (i + 1 >= argc) {
std::cerr << "essim: --source needs a filename\n";
return 2;
}
boot_source = argv[++i];
continue;
}
if (a == "--restore") {
if (i + 1 >= argc) {
std::cerr << "essim: --restore needs a filename\n";
return 2;
}
boot_restore = argv[++i];
continue;
}
if (a == "--batch") {
batch = true;
continue;
}
if (a == "--help" || a == "-h") {
print_usage(argv[0]);
return 0;
}
std::cerr << "essim: unknown option: " << a << "\n";
print_usage(argv[0]);
return 2;
}
// Order matters: a `--restore` brings up a snapshot, then `--source`
// can layer additional commands on top of it (useful e.g. for "load
// snapshot, then re-run a small script that adds a new card").
if (!boot_restore.empty()) fe.BootDispatch("restore " + boot_restore);
if (!boot_source.empty()) fe.BootDispatch("source " + boot_source);
// Batch mode: the boot dispatch already ran synchronously (no event loop
// yet), so the console output is complete. Print it and exit.
if (batch) {
fe.DumpOutput(std::cout);
return 0;
}
fe.Run();
return 0;
}

View File

@@ -0,0 +1,13 @@
#ifndef _FRONTEND_MAIN_HPP_
#define _FRONTEND_MAIN_HPP_
class Frontend;
// Shared process entry point, frontend-agnostic. Parses argv
// (--source / --restore / --batch / --commands-md / --help), drives `fe`
// through the boot commands and then either dumps output (batch) or enters its
// event loop, and returns the process exit code. Each frontend's main() just
// constructs its concrete Frontend and forwards to this.
int frontend_main(int argc, char **argv, Frontend &fe);
#endif // _FRONTEND_MAIN_HPP_

View File

@@ -32,7 +32,7 @@ target_link_libraries(essim_tui
)
add_executable(essim "${CMAKE_CURRENT_SOURCE_DIR}/main.cpp")
target_link_libraries(essim PRIVATE essim_tui)
target_link_libraries(essim PRIVATE essim_tui essim_frontend)
# Keep the binary at the top of the build tree (./build/essim), regardless of
# which frontend subdir produced it.

View File

@@ -1,98 +1,11 @@
#include "frontends/frontend_main.hpp"
#include "frontends/tui/tui.hpp"
#include <fstream>
#include <iostream>
#include <string>
namespace {
void print_usage(const char *prog) {
std::cerr <<
"usage: " << prog << " [--batch] [--source FILE] [--restore FILE]\n"
" " << prog << " --commands-md [FILE]\n"
" " << prog << " --help\n"
" (no args) launch the TUI on an empty system.\n"
" --source FILE after boot, run FILE as an essim script\n"
" (one command per line; same as the `source`\n"
" command). Output is in the console screen.\n"
" --restore FILE after boot, restore the system snapshot in\n"
" FILE (same as the `restore` command).\n"
" Combine with --source to layer a script on\n"
" top of a restored snapshot.\n"
" --batch run --restore/--source, print the console\n"
" output to stdout, and exit without the TUI.\n"
" --commands-md [FILE] dump the command registry as Markdown.\n"
" With FILE: write there. Without: stdout.\n"
" (Used by `cmake --build build --target doc`.)\n"
" --help, -h show this help.\n";
}
} // namespace
// The TUI frontend's entry point: construct the concrete Frontend (Tui) and
// hand off to the shared, frontend-agnostic launcher. All argv parsing and the
// boot/batch/run flow live in frontend_main(); a second frontend's main() looks
// exactly like this with its own Frontend type.
int main(int argc, char **argv) {
std::string boot_restore;
std::string boot_source;
bool batch = false;
for (int i = 1; i < argc; ++i) {
std::string a = argv[i];
if (a == "--commands-md") {
Tui tui;
if (i + 1 < argc) {
std::ofstream f(argv[++i]);
if (!f) {
std::cerr << "essim: cannot open " << argv[i] << " for writing\n";
return 1;
}
tui.DumpCommandsMd(f);
} else {
tui.DumpCommandsMd(std::cout);
}
return 0;
}
if (a == "--source") {
if (i + 1 >= argc) {
std::cerr << "essim: --source needs a filename\n";
return 2;
}
boot_source = argv[++i];
continue;
}
if (a == "--restore") {
if (i + 1 >= argc) {
std::cerr << "essim: --restore needs a filename\n";
return 2;
}
boot_restore = argv[++i];
continue;
}
if (a == "--batch") {
batch = true;
continue;
}
if (a == "--help" || a == "-h") {
print_usage(argv[0]);
return 0;
}
std::cerr << "essim: unknown option: " << a << "\n";
print_usage(argv[0]);
return 2;
}
Tui tui;
// Order matters: a `--restore` brings up a snapshot, then `--source`
// can layer additional commands on top of it (useful e.g. for "load
// snapshot, then re-run a small script that adds a new card").
if (!boot_restore.empty()) tui.BootDispatch("restore " + boot_restore);
if (!boot_source.empty()) tui.BootDispatch("source " + boot_source);
// Batch mode: the boot dispatch already ran synchronously (no screen yet),
// so the console output is complete. Print it and exit without the TUI.
if (batch) {
tui.DumpOutput(std::cout);
return 0;
}
tui.Run();
return 0;
return frontend_main(argc, argv, tui);
}

View File

@@ -13,9 +13,11 @@
#include <ftxui/component/component.hpp>
#include <ftxui/component/screen_interactive.hpp>
#include "frontends/frontend.hpp"
class System;
class Tui {
class Tui : public Frontend {
enum class Completion { None, Path, Command };
struct Prompt {
@@ -198,16 +200,16 @@ private:
public:
Tui();
~Tui();
void Run();
void DumpCommandsMd(std::ostream &out) const;
void Run() override;
void DumpCommandsMd(std::ostream &out) const override;
// Write the accumulated console output to `out`. Used by batch mode to
// surface a script's output without starting the TUI.
void DumpOutput(std::ostream &out) const;
void DumpOutput(std::ostream &out) const override;
// Boot-time hook: dispatch a single command exactly as if the user
// typed it (e.g. `restore foo.essim` or `source bring-up.essim`).
// Call before `Run()` to seed the system before the event loop starts.
void BootDispatch(const std::string &raw);
void BootDispatch(const std::string &raw) override;
private:
// Lifecycle (commands.cpp)