diff --git a/CMakeLists.txt b/CMakeLists.txt index bbdc32b..278da8f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/src/frontends/frontend.hpp b/src/frontends/frontend.hpp new file mode 100644 index 0000000..6c32f0f --- /dev/null +++ b/src/frontends/frontend.hpp @@ -0,0 +1,30 @@ +#ifndef _FRONTEND_HPP_ +#define _FRONTEND_HPP_ + +#include +#include + +// 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_ diff --git a/src/frontends/frontend_main.cpp b/src/frontends/frontend_main.cpp new file mode 100644 index 0000000..dbd99d4 --- /dev/null +++ b/src/frontends/frontend_main.cpp @@ -0,0 +1,99 @@ +#include "frontends/frontend_main.hpp" + +#include "frontends/frontend.hpp" + +#include +#include +#include + +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; +} diff --git a/src/frontends/frontend_main.hpp b/src/frontends/frontend_main.hpp new file mode 100644 index 0000000..24a80ee --- /dev/null +++ b/src/frontends/frontend_main.hpp @@ -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_ diff --git a/src/frontends/tui/CMakeLists.txt b/src/frontends/tui/CMakeLists.txt index 684e03f..9f31b6d 100644 --- a/src/frontends/tui/CMakeLists.txt +++ b/src/frontends/tui/CMakeLists.txt @@ -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. diff --git a/src/frontends/tui/main.cpp b/src/frontends/tui/main.cpp index d3011f2..f23144d 100644 --- a/src/frontends/tui/main.cpp +++ b/src/frontends/tui/main.cpp @@ -1,98 +1,11 @@ +#include "frontends/frontend_main.hpp" #include "frontends/tui/tui.hpp" -#include -#include -#include - -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); } diff --git a/src/frontends/tui/tui.hpp b/src/frontends/tui/tui.hpp index 75e5ac0..8744b4a 100644 --- a/src/frontends/tui/tui.hpp +++ b/src/frontends/tui/tui.hpp @@ -13,9 +13,11 @@ #include #include +#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)