deargui-vpl/docs/cli.md

572 lines
15 KiB
Markdown

# CLI Architecture Options
## Current State
Currently, both GUI and console variants create the full UI:
- Console variant = GUI variant + visible console window
- All ImGui/rendering initialization still happens
- Window is created even if you just want to process files
**Problem:** No true headless/CLI mode for automation, testing, or server environments.
## Goal
Enable true CLI operation:
- Process graphs without creating windows
- Validate, convert, export without UI overhead
- Reuse existing CLI argument parsing from `entry_point.cpp`
- Keep GUI and CLI code maintainable
## Architecture Options
### Option 1: Headless Mode Flag ⭐ (Recommended for Quick Implementation)
**Concept:** Add a `--headless` flag that skips all UI initialization.
**Pros:**
- Minimal code changes
- Reuses all existing infrastructure
- Single codebase for both modes
- Easy to maintain
**Cons:**
- Application class still assumes UI might exist
- Some refactoring needed to make UI optional
- Not the cleanest separation
**Implementation:**
```cpp
// blueprints-example.cpp
int Main(const ArgsMap& args)
{
// Check for headless mode
bool headless = false;
auto it = args.find("headless");
if (it != args.end() && it->second.Type == ArgValue::Type::Bool && it->second.Bool) {
headless = true;
}
if (headless) {
// CLI mode - no UI
return RunCLI(args);
} else {
// GUI mode - existing code
App example("Blueprints", args);
if (example.Create())
return example.Run();
return 0;
}
}
int RunCLI(const ArgsMap& args)
{
// Load graph
std::string filename = GetStringArg(args, "file", "");
if (filename.empty()) {
fprintf(stderr, "Error: --file required in headless mode\n");
return 1;
}
// Process without UI
GraphState graph;
if (!graph.Load(filename)) {
fprintf(stderr, "Error: Failed to load %s\n", filename.c_str());
return 1;
}
// Perform operations
std::string command = GetStringArg(args, "command", "validate");
if (command == "validate") {
return ValidateGraph(graph);
} else if (command == "export") {
std::string output = GetStringArg(args, "output", "");
return ExportGraph(graph, output);
}
// ... more commands
return 0;
}
```
**Entry point update:**
```cpp
// entry_point.cpp - add to CLI parser
app.add_option("--headless", "Run without GUI")->capture_default_str();
app.add_option("--command", "Command to execute (validate, export, etc.)")->capture_default_str();
app.add_option("--output", "Output file path")->capture_default_str();
```
**Usage:**
```bash
# Validate graph
blueprints-example-console.exe --headless --file graph.json --command validate
# Export to different format
blueprints-example-console.exe --headless --file graph.json --command export --output graph.xml
# GUI mode (existing)
blueprints-example.exe --file graph.json
```
---
### Option 2: Separate CLI Executable
**Concept:** Create `blueprints-cli` as a completely separate executable.
**Pros:**
- Clean separation
- No UI dependencies in CLI build
- Smaller executable for CLI
- Clear intent (different binary = different purpose)
**Cons:**
- Code duplication risk
- Two build targets to maintain
- Shared code must be extracted to library
**Implementation:**
```cmake
# CMakeLists.txt
add_library(blueprints-core STATIC
core/graph_state.cpp
blocks/block.cpp
# ... all non-UI code
)
# GUI application
add_example_executable(blueprints-example
blueprints-example.cpp
app.cpp
# ... UI files
)
target_link_libraries(blueprints-example PRIVATE blueprints-core)
# CLI application
add_executable(blueprints-cli
cli/blueprints-cli.cpp
cli/commands.cpp
)
target_link_libraries(blueprints-cli PRIVATE blueprints-core)
# Note: No imgui, no application library
```
```cpp
// cli/blueprints-cli.cpp
int main(int argc, char** argv)
{
CLI::App app{"Blueprints CLI"};
std::string file;
std::string command;
std::string output;
app.add_option("-f,--file", file, "Input graph file")->required();
app.add_option("-c,--command", command, "Command")->required();
app.add_option("-o,--output", output, "Output file");
CLI11_PARSE(app, argc, argv);
// Execute command
return ExecuteCommand(command, file, output);
}
```
**Usage:**
```bash
blueprints-cli -f graph.json -c validate
blueprints-cli -f graph.json -c export -o graph.xml
blueprints-example --file graph.json # GUI
```
---
### Option 3: Application Base Class Architecture ⭐⭐ (Recommended for Long-term)
**Concept:** Separate core logic from presentation.
**Pros:**
- Clean architecture
- Core testable without UI
- Natural separation of concerns
- Easy to add new frontends (Web, TUI, etc.)
**Cons:**
- Requires significant refactoring
- More files to maintain
- Need to identify what's "core" vs "UI"
**Implementation:**
```cpp
// core/blueprints_engine.h
class BlueprintsEngine {
public:
BlueprintsEngine();
virtual ~BlueprintsEngine() = default;
// Core operations (no UI)
bool LoadGraph(const std::string& filename);
bool SaveGraph(const std::string& filename);
bool ValidateGraph();
bool ExecuteGraph();
std::string ExportGraph(const std::string& format);
GraphState& GetGraph() { return m_Graph; }
protected:
GraphState m_Graph;
// ... core state
};
// app/blueprints_app.h
class BlueprintsApp : public BlueprintsEngine {
public:
BlueprintsApp(const char* name, const ArgsMap& args);
// UI operations
bool Create();
int Run();
void OnFrame(float deltaTime);
private:
Application m_Application;
ax::NodeEditor::EditorContext* m_Editor;
// ... UI state
// UI methods
void RenderNodes();
void HandleInput();
};
// cli/blueprints_cli.h
class BlueprintsCLI : public BlueprintsEngine {
public:
BlueprintsCLI(const ArgsMap& args);
int Execute();
private:
ArgsMap m_Args;
int CommandValidate();
int CommandExport();
int CommandExecute();
};
```
```cpp
// blueprints-example.cpp (GUI)
int Main(const ArgsMap& args)
{
BlueprintsApp app("Blueprints", args);
if (app.Create())
return app.Run();
return 0;
}
// blueprints-cli.cpp (CLI)
int Main(const ArgsMap& args)
{
BlueprintsCLI cli(args);
return cli.Execute();
}
```
**Usage:**
```bash
# Same executable, mode determined by arguments
blueprints-example-console.exe --headless --file graph.json --command validate
blueprints-example.exe --file graph.json # GUI mode
```
---
### Option 4: Composition Pattern
**Concept:** Core engine is a separate component, Application "has-a" engine.
**Pros:**
- Very clean separation
- Engine easily testable
- Can swap out UI completely
- Multiple UIs can share same engine
**Cons:**
- More indirection
- Need to manage engine lifetime
- Communication between engine and UI
**Implementation:**
```cpp
// core/graph_engine.h
class GraphEngine {
public:
struct Config {
std::string filename;
bool autoSave = false;
// ... options
};
GraphEngine(const Config& config);
bool Load();
bool Save();
bool Validate();
void Execute();
// Query state
const GraphState& GetState() const { return m_State; }
// Modify state
void AddNode(const NodeConfig& config);
void RemoveNode(NodeId id);
void AddLink(PinId from, PinId to);
private:
Config m_Config;
GraphState m_State;
};
// app/blueprints_app.h
class BlueprintsApp {
public:
BlueprintsApp(std::unique_ptr<GraphEngine> engine);
bool Create();
int Run();
private:
std::unique_ptr<GraphEngine> m_Engine;
Application m_Application;
// UI renders m_Engine->GetState()
};
// cli/blueprints_cli.h
class BlueprintsCLI {
public:
BlueprintsCLI(std::unique_ptr<GraphEngine> engine, const ArgsMap& args);
int Execute();
private:
std::unique_ptr<GraphEngine> m_Engine;
ArgsMap m_Args;
};
```
```cpp
// blueprints-example.cpp
int Main(const ArgsMap& args)
{
bool headless = GetBoolArg(args, "headless", false);
GraphEngine::Config config;
config.filename = GetStringArg(args, "file", "");
auto engine = std::make_unique<GraphEngine>(config);
if (headless) {
BlueprintsCLI cli(std::move(engine), args);
return cli.Execute();
} else {
BlueprintsApp app(std::move(engine));
if (app.Create())
return app.Run();
return 0;
}
}
```
---
### Option 5: Command Pattern
**Concept:** CLI commands are objects, both GUI and CLI execute commands.
**Pros:**
- Undo/redo support naturally
- Commands are testable
- GUI and CLI share exact same logic
- Easy to add scripting
**Cons:**
- Everything must be a command
- More abstraction overhead
- Complex for simple operations
**Implementation:**
```cpp
// commands/command.h
class Command {
public:
virtual ~Command() = default;
virtual bool Execute(GraphState& state) = 0;
virtual std::string GetName() const = 0;
};
// commands/validate_command.h
class ValidateCommand : public Command {
public:
bool Execute(GraphState& state) override {
// Validation logic
return ValidateGraphImpl(state);
}
std::string GetName() const override { return "validate"; }
};
// commands/export_command.h
class ExportCommand : public Command {
public:
ExportCommand(const std::string& outputPath, const std::string& format)
: m_OutputPath(outputPath), m_Format(format) {}
bool Execute(GraphState& state) override {
return ExportGraphImpl(state, m_OutputPath, m_Format);
}
std::string GetName() const override { return "export"; }
private:
std::string m_OutputPath;
std::string m_Format;
};
// cli/command_runner.h
class CommandRunner {
public:
CommandRunner(GraphState& state) : m_State(state) {}
bool Run(std::unique_ptr<Command> cmd) {
printf("Executing: %s\n", cmd->GetName().c_str());
return cmd->Execute(m_State);
}
private:
GraphState& m_State;
};
```
```cpp
// blueprints-cli.cpp
int Main(const ArgsMap& args)
{
bool headless = GetBoolArg(args, "headless", false);
std::string filename = GetStringArg(args, "file", "");
GraphState state;
if (!state.Load(filename)) {
fprintf(stderr, "Failed to load graph\n");
return 1;
}
if (headless) {
std::string command = GetStringArg(args, "command", "validate");
CommandRunner runner(state);
std::unique_ptr<Command> cmd;
if (command == "validate") {
cmd = std::make_unique<ValidateCommand>();
} else if (command == "export") {
std::string output = GetStringArg(args, "output", "");
std::string format = GetStringArg(args, "format", "json");
cmd = std::make_unique<ExportCommand>(output, format);
}
return runner.Run(std::move(cmd)) ? 0 : 1;
} else {
// GUI mode - commands triggered by UI actions
BlueprintsApp app(state);
return app.Run();
}
}
```
---
## Comparison Matrix
| Criteria | Option 1<br>Headless Flag | Option 2<br>Separate Exe | Option 3<br>Base Class | Option 4<br>Composition | Option 5<br>Commands |
|----------|----------|----------|----------|----------|----------|
| **Implementation Effort** | ⭐⭐⭐⭐⭐ Easy | ⭐⭐⭐ Moderate | ⭐⭐ Hard | ⭐⭐ Hard | ⭐ Very Hard |
| **Maintainability** | ⭐⭐⭐ Good | ⭐⭐⭐ Good | ⭐⭐⭐⭐ Great | ⭐⭐⭐⭐⭐ Excellent | ⭐⭐⭐ Good |
| **Code Reuse** | ⭐⭐⭐⭐⭐ Excellent | ⭐⭐⭐ Good | ⭐⭐⭐⭐ Great | ⭐⭐⭐⭐⭐ Excellent | ⭐⭐⭐⭐⭐ Excellent |
| **Testing** | ⭐⭐ Fair | ⭐⭐⭐⭐ Great | ⭐⭐⭐⭐⭐ Excellent | ⭐⭐⭐⭐⭐ Excellent | ⭐⭐⭐⭐⭐ Excellent |
| **Clean Separation** | ⭐⭐ Fair | ⭐⭐⭐⭐⭐ Excellent | ⭐⭐⭐⭐ Great | ⭐⭐⭐⭐⭐ Excellent | ⭐⭐⭐⭐ Great |
| **Binary Size (CLI)** | Large (full UI) | ⭐⭐⭐⭐⭐ Small | Medium | Medium | Medium |
| **Flexibility** | ⭐⭐⭐ Good | ⭐⭐⭐ Good | ⭐⭐⭐⭐ Great | ⭐⭐⭐⭐⭐ Excellent | ⭐⭐⭐⭐⭐ Excellent |
## Recommended Approach
### Phase 1: Quick Win (Option 1)
Start with **Headless Flag** approach:
- Immediate CLI functionality
- Minimal code changes
- Validates the CLI use cases
### Phase 2: Refactor (Option 3 or 4)
Once CLI use cases are clear:
- Extract core logic
- Either use **Base Class** (if inheritance makes sense) or **Composition** (if more flexibility needed)
- Better testing and separation
### Phase 3: Advanced (Optional)
If you need undo/redo, scripting, or complex workflows:
- Add **Command Pattern** on top of Phase 2
## CLI Argument Reuse
All options can reuse `entry_point.cpp` CLI parsing:
```cpp
// entry_point.cpp - add CLI options
app.add_flag("--headless", "Run without GUI");
app.add_option("--command", "Command to execute")->capture_default_str();
app.add_option("--output", "Output file path")->capture_default_str();
app.add_option("--format", "Output format")->capture_default_str();
app.add_flag("--validate", "Validate graph and exit");
app.add_flag("--execute", "Execute graph and exit");
```
The `ArgsMap` is already passed to `Main()`, so all these arguments are available.
## Example CLI Commands
```bash
# Validate
blueprints-console.exe --headless --file graph.json --command validate
# Execute headless
blueprints-console.exe --headless --file graph.json --command execute
# Export to XML
blueprints-console.exe --headless --file graph.json --command export --output graph.xml --format xml
# Convert format
blueprints-console.exe --headless --file graph.json --command convert --output graph.yaml
# Batch processing
for file in *.json; do
blueprints-console.exe --headless --file "$file" --command validate
done
```
## Next Steps
1. **Decision:** Choose starting approach (recommend Option 1 for quick start)
2. **Identify:** List specific CLI operations needed (validate, export, execute, convert, etc.)
3. **Implement:** Add headless flag and CLI operations
4. **Test:** Verify CLI mode works without creating windows
5. **Refactor:** If needed, move to Option 3 or 4 for better architecture
## See Also
- [Console Variants Guide](console-variants.md)
- [CMake Options](cmake-options.md)
- [Debugging Guide](../DEBUGGING.md)