deargui-vpl/docs/cli.md

15 KiB

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

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:

// 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:

// 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:

# 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:

# 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
// 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:

blueprints-cli -f graph.json -c validate
blueprints-cli -f graph.json -c export -o graph.xml
blueprints-example --file graph.json  # GUI

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:

// 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();
};
// 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:

# 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:

// 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;
};
// 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:

// 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;
};
// 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
Headless Flag
Option 2
Separate Exe
Option 3
Base Class
Option 4
Composition
Option 5
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

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:

// 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

# 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