Skip to content

Update Launch UX With Default + Detach Modes#36

Merged
K1ngst0m merged 3 commits intomainfrom
dev/update-launch-modes
Jan 8, 2026
Merged

Update Launch UX With Default + Detach Modes#36
K1ngst0m merged 3 commits intomainfrom
dev/update-launch-modes

Conversation

@K1ngst0m
Copy link
Copy Markdown
Collaborator

@K1ngst0m K1ngst0m commented Jan 8, 2026

User description

Why

Launching a target app with Goggles' “full feature” workflow currently requires users to manually provide environment variables that are not stable or user-friendly (e.g., nested DISPLAY/WAYLAND_DISPLAY values and WSI proxy sizing). This is error-prone, hard to remember, and not suitable for packaging/Steam launch options.

What Changes

  • Add two run modes for the Goggles launch workflow:
    • Default mode (no --detach): “full feature” launch mode intended for typical use and packaging.
    • Detach mode (--detach): viewer-only mode intended for advanced/manual launching.
  • In default mode:
    • Input forwarding is enabled and required.
    • GOGGLES_WSI_PROXY=1 is forced for the launched app.
    • DISPLAY and WAYLAND_DISPLAY are set for the launched app (both are provided).
    • --app-width and --app-height are supported and only apply here (mapped to GOGGLES_WIDTH/GOGGLES_HEIGHT).
  • In detach mode:
    • Input forwarding is disabled.
    • Default-mode-only options are rejected (--app-width, --app-height).
  • Do not expose a --wsi-proxy toggle; WSI proxy is implicit in default mode.
  • Ensure the launch flow continues to use configuration via config/goggles.toml (and --config overrides) for viewer behavior.

Impact

  • Affected specs:
    • app-window (CLI + launch-mode UX behavior)
  • Affected code (expected):
    • src/app/cli.hpp / src/app/main.cpp / src/app/application.* (mode parsing + orchestration)
    • scripts/task/start.sh (optional: simplify to use new mode, reduce manual env usage)
    • docs/ (update user-facing launch guidance)

Non-Goals

  • Changing Vulkan layer behavior or its environment variable contracts (only how the app sets them by default).
  • Introducing a third “backend selection” mode (X11 vs Wayland); default mode always provides both.

Open Questions

  • Should the app command be required in default mode (packaging/Steam wrapper use), or optional (viewer-only default)?
    • This proposal assumes default mode is used for the “full feature” launch path and therefore expects an app command to be provided.

PR Type

Enhancement


Description

  • Add --detach flag for viewer-only mode and default mode for full-feature app launching

  • Implement process lifecycle management with automatic app spawning and environment setup

  • Add --app-width and --app-height CLI options for WSI proxy virtual surface sizing

  • Enforce strict CLI option dependencies (detach mode rejects app sizing and input forwarding)

  • Simplify launch workflow by automatically setting GOGGLES_CAPTURE, GOGGLES_WSI_PROXY, DISPLAY, and WAYLAND_DISPLAY


Diagram Walkthrough

flowchart LR
  CLI["CLI Parser<br/>--detach flag<br/>-- separator<br/>--app-width/height"]
  DEFAULT["Default Mode<br/>Input forwarding enabled<br/>App command required"]
  DETACH["Detach Mode<br/>Viewer-only<br/>No app command"]
  SPAWN["Spawn Target App<br/>Set env vars<br/>GOGGLES_CAPTURE=1<br/>GOGGLES_WSI_PROXY=1<br/>DISPLAY/WAYLAND_DISPLAY"]
  LIFECYCLE["Process Lifecycle<br/>Exit when app exits<br/>Terminate app on close"]
  
  CLI -->|"no --detach"| DEFAULT
  CLI -->|"--detach"| DETACH
  DEFAULT --> SPAWN
  SPAWN --> LIFECYCLE
  DETACH -->|"run viewer"| LIFECYCLE
Loading

File Walkthrough

Relevant files
Enhancement
6 files
cli.hpp
Add detach mode and app sizing CLI options                             
+106/-3 
cli.cpp
Implement CLI parsing with mode validation                             
application.hpp
Add display accessor methods for app spawning                       
+3/-0     
application.cpp
Implement x11_display and wayland_display accessors           
+14/-0   
main.cpp
Implement app spawning and process lifecycle management   
+128/-5 
start.sh
Simplify launch script to use new mode semantics                 
+13/-98 
Tests
1 files
test_cli.cpp
Add comprehensive CLI parsing tests                                           
+99/-0   
Configuration changes
2 files
CMakeLists.txt
Register CLI tests in build system                                             
+3/-0     
goggles.toml
Update default shader preset selection                                     
+1/-1     
Documentation
6 files
README.md
Update usage examples for new launch modes                             
+17/-16 
input_forwarding.md
Document preferred default mode app launching                       
+6/-11   
proposal.md
Add formal change proposal documentation                                 
+41/-0   
design.md
Add design notes for mode implementation                                 
+43/-0   
spec.md
Add formal requirements and scenarios                                       
+38/-0   
tasks.md
Add implementation task checklist                                               
+34/-0   

Summary by CodeRabbit

  • New Features

    • Detach mode to run the viewer independently; viewer-only mode clarified.
    • --app-width / --app-height to set virtual display size.
  • Changes

    • Unified command syntax requiring a "--" separator for target app args.
    • Viewer now exits/affects target lifetime behavior.
    • Default shader preset updated for improved visuals.
  • Documentation

    • Updated usage examples, launch workflow, Steam guidance, and deprecated topic removal.
  • Tests

    • Added CLI parser tests covering new modes and validation.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Jan 8, 2026

📝 Walkthrough

Walkthrough

The pull request adds Default and Detach launch modes for the Goggles viewer: Default spawns and manages a target app with environment wiring (GOGGLES_CAPTURE, GOGGLES_WSI_PROXY, DISPLAY, WAYLAND_DISPLAY, optional GOGGLES_WIDTH/HEIGHT) and monitors its lifecycle; Detach runs the viewer standalone. CLI and start script parsing were refactored; Application gains display accessors.

Changes

Cohort / File(s) Summary
Documentation & Specs
README.md, docs/input_forwarding.md, openspec/changes/update-launch-modes-default-detach/*
Updated docs and examples to the single-command launch pattern and Default/Detach semantics; added design/proposal/spec/tasks describing mode rules, env wiring, CLI constraints, and lifecycle behaviors.
Configuration
config/goggles.toml
Switched default shader preset from shaders/retroarch/crt/crt-royale.slangp to shaders/retroarch/crt/zfast-crt.slangp.
CLI Argument Parsing
src/app/cli.hpp
Added --detach, --app-width, --app-height, and collection of app command after --; introduced validation enforcing mode-specific constraints and explicit parse errors.
Application Interface
src/app/application.hpp, src/app/application.cpp
Added x11_display() and wayland_display() accessors exposing display strings from the input forwarder.
Main & Process Management
src/app/main.cpp
Added spawn/monitor/terminate logic for target app (env injection, posix_spawnp usage), integrated monitoring into viewer loop, return target exit code; retain detach path where viewer runs without spawning target.
Launch Script
scripts/task/start.sh
Simplified start task: collect remaining args as viewer (goggles) args and exec viewer; removed separator handling, per-env parsing, lifecycle/trap logic.
Tests
tests/app/test_cli.cpp, tests/CMakeLists.txt
Added CLI parser unit tests for detach/default modes, separator presence, app command parsing, and width/height validation; included test utilities.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant CLI as CLI Parser
    participant App as Application
    participant Viewer as Viewer Loop
    participant Target as Target App Process
    participant Monitor as Process Monitor

    User->>CLI: goggles -- ./target arg1 arg2
    CLI->>CLI: Parse args (default mode), validate separator & command
    CLI->>App: Start with detach=false, app dimensions?
    App->>App: Enable input forwarding
    App->>Viewer: Start viewer event loop
    Viewer->>Target: spawn_target_app() with env (DISPLAY, WAYLAND_DISPLAY, GOGGLES_*)
    Target->>Target: Execute target app
    Viewer->>Monitor: Poll/monitor child process
    Target->>Monitor: Exit with status
    Monitor->>Viewer: Notify child exit
    Viewer->>Viewer: Shutdown viewer loop
    Viewer->>Target: Ensure termination if still running
    Viewer->>User: Return target app exit code
Loading
sequenceDiagram
    actor User
    participant CLI as CLI Parser
    participant App as Application
    participant Viewer as Viewer Loop

    User->>CLI: goggles --detach --config myconfig.toml
    CLI->>CLI: Parse args (detach mode), validate no app command/widths
    CLI->>App: Start with detach=true
    App->>App: Disable input forwarding
    App->>Viewer: Start viewer event loop (no target)
    User->>Viewer: Interact and close viewer
    Viewer->>User: Exit gracefully
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

Review effort 4/5

Poem

🐰 In burrows bright I tap my feet,

I stitched two modes for you to meet.
Default hums and spawns with care,
Detach hops solo through the air,
Goggles and I — a hopping feat!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main feature change: introducing default and detach launch modes for the Goggles launcher.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Streamline the user workflow by making Goggles launch the target app directly with capture.

- Add `--detach` flag for viewer-only mode (manual target launch)
- Default mode spawns target app with proper capture and compositor environment
- Add `--app-width` and `--app-height` CLI options for WSI proxy control
- Implement process lifecycle management (exit when app exits or terminate app on close)
- Require `--` separator to avoid parsing app args as viewer options
- Add CLI parsing tests and update all documentation
@K1ngst0m K1ngst0m marked this pull request as ready for review January 8, 2026 09:08
@qodo-code-review
Copy link
Copy Markdown

qodo-code-review bot commented Jan 8, 2026

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
PATH search execution

Description: The new default-mode launcher uses execvp(argv[0], ...) with a user-controlled argv[0]
(from cli_opts.app_command), which searches PATH and can be exploited to run an unintended
binary if Goggles is ever executed with elevated privileges or in a manipulated
environment (e.g., attacker prepends a directory containing a malicious vkcube to PATH).
main.cpp [17-55]

Referred Code
static auto spawn_target_app(const std::vector<std::string>& command,
                             const std::string& x11_display, const std::string& wayland_display,
                             uint32_t app_width, uint32_t app_height) -> goggles::Result<pid_t> {
    if (command.empty()) {
        return goggles::make_error<pid_t>(goggles::ErrorCode::invalid_config,
                                          "missing target app command");
    }

    if (x11_display.empty() || wayland_display.empty()) {
        return goggles::make_error<pid_t>(goggles::ErrorCode::input_init_failed,
                                          "input forwarding display information unavailable");
    }

    pid_t pid = fork();
    if (pid < 0) {
        return goggles::make_error<pid_t>(goggles::ErrorCode::unknown_error, "fork() failed");
    }

    if (pid == 0) {
        setenv("GOGGLES_CAPTURE", "1", 1);
        setenv("GOGGLES_WSI_PROXY", "1", 1);


 ... (clipped 18 lines)
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

🔴
Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Potential indefinite hang: terminate_child unconditionally calls blocking waitpid after SIGTERM without
timeout/escalation or error handling, which can hang the viewer indefinitely if the child
ignores/does not handle SIGTERM.

Referred Code
static auto terminate_child(pid_t pid) -> void {
    if (pid <= 0) {
        return;
    }
    kill(pid, SIGTERM);
    int status = 0;
    waitpid(pid, &status, 0);
}

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status:
Incomplete audit context: The new target-app launch lifecycle is logged (e.g., PID) but does not include any user
identity/context fields required for an audit trail, which may or may not be applicable
depending on product requirements.

Referred Code
    GOGGLES_LOG_INFO("Launched target app (pid={})", child_pid);
}

while (app->is_running()) {
    app->pump_events();
    app->tick_frame();

    if (child_pid > 0 && !child_exited) {
        pid_t result = waitpid(child_pid, &child_status, WNOHANG);
        if (result == child_pid) {
            child_exited = true;
            push_quit_event();
        }
    }
}

if (child_pid > 0 && !child_exited) {
    GOGGLES_LOG_INFO("Viewer exited; terminating target app (pid={})", child_pid);
    terminate_child(child_pid);

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status:
Log format unclear: The new log lines (e.g., launched PID / termination messages) appear non-sensitive, but it
is not verifiable from the diff whether the underlying logger is structured (e.g., JSON)
as required by the checklist.

Referred Code
        GOGGLES_LOG_CRITICAL("Failed to launch target app: {} ({})",
                             spawn_result.error().message,
                             goggles::error_code_name(spawn_result.error().code));
        return EXIT_FAILURE;
    }
    child_pid = spawn_result.value();
    GOGGLES_LOG_INFO("Launched target app (pid={})", child_pid);
}

while (app->is_running()) {
    app->pump_events();
    app->tick_frame();

    if (child_pid > 0 && !child_exited) {
        pid_t result = waitpid(child_pid, &child_status, WNOHANG);
        if (result == child_pid) {
            child_exited = true;
            push_quit_event();
        }
    }
}


 ... (clipped 4 lines)

Learn more about managing compliance generic rules or creating your own custom rules

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review bot commented Jan 8, 2026

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Replace fork+exec with posix_spawn
Suggestion Impact:The commit removed the fork/execvp pattern and implemented process creation via posix_spawnp(), including building argv and an envp array for the child (merging current environ while overriding specific keys). This fulfills the core intent of the suggestion, though the environment handling and error reporting differ from the suggested snippet.

code diff:

-    pid_t pid = fork();
-    if (pid < 0) {
-        return goggles::make_error<pid_t>(goggles::ErrorCode::unknown_error, "fork() failed");
-    }
-
-    if (pid == 0) {
-        setenv("GOGGLES_CAPTURE", "1", 1);
-        setenv("GOGGLES_WSI_PROXY", "1", 1);
-        setenv("DISPLAY", x11_display.c_str(), 1);
-        setenv("WAYLAND_DISPLAY", wayland_display.c_str(), 1);
-
-        if (app_width != 0 && app_height != 0) {
-            setenv("GOGGLES_WIDTH", std::to_string(app_width).c_str(), 1);
-            setenv("GOGGLES_HEIGHT", std::to_string(app_height).c_str(), 1);
-        }
-
-        std::vector<char*> argv;
-        argv.reserve(command.size() + 1);
-        for (const auto& arg : command) {
-            argv.push_back(const_cast<char*>(arg.c_str()));
-        }
-        argv.push_back(nullptr);
-
-        execvp(argv[0], argv.data());
-        _exit(127);
+    auto is_override_key = [](std::string_view key) -> bool {
+        return key == "GOGGLES_CAPTURE" || key == "GOGGLES_WSI_PROXY" || key == "DISPLAY" ||
+               key == "WAYLAND_DISPLAY" || key == "GOGGLES_WIDTH" || key == "GOGGLES_HEIGHT";
+    };
+
+    std::vector<std::string> env_overrides;
+    env_overrides.reserve(6);
+    env_overrides.emplace_back("GOGGLES_CAPTURE=1");
+    env_overrides.emplace_back("GOGGLES_WSI_PROXY=1");
+    env_overrides.emplace_back("DISPLAY=" + x11_display);
+    env_overrides.emplace_back("WAYLAND_DISPLAY=" + wayland_display);
+
+    if (app_width != 0 && app_height != 0) {
+        env_overrides.emplace_back("GOGGLES_WIDTH=" + std::to_string(app_width));
+        env_overrides.emplace_back("GOGGLES_HEIGHT=" + std::to_string(app_height));
+    }
+
+    std::vector<char*> envp;
+    envp.reserve(env_overrides.size() + 64);
+    for (auto& entry : env_overrides) {
+        envp.push_back(entry.data());
+    }
+    for (char** entry = environ; entry != nullptr && *entry != nullptr; ++entry) {
+        std::string_view kv{*entry};
+        const auto eq = kv.find('=');
+        if (eq != std::string_view::npos) {
+            const auto key = kv.substr(0, eq);
+            if (is_override_key(key)) {
+                continue;
+            }
+        }
+        envp.push_back(*entry);
+    }
+    envp.push_back(nullptr);
+
+    std::vector<char*> argv;
+    argv.reserve(command.size() + 1);
+    for (const auto& arg : command) {
+        argv.push_back(const_cast<char*>(arg.c_str()));
+    }
+    argv.push_back(nullptr);
+
+    pid_t pid = -1;
+    const int rc = posix_spawnp(&pid, argv[0], nullptr, nullptr, argv.data(), envp.data());
+    if (rc != 0) {
+        return goggles::make_error<pid_t>(goggles::ErrorCode::unknown_error,
+                                          std::string("posix_spawnp() failed: ") +
+                                              std::strerror(rc));
     }
 
     return pid;

Replace the fork() and execvp() pattern with posix_spawnp() to safely spawn the
child process. This avoids calling non-async-signal-safe functions after forking
in a multi-threaded context, preventing potential deadlocks.

src/app/main.cpp [30-55]

-pid_t pid = fork();
-if (pid < 0) {
-    return goggles::make_error<pid_t>(goggles::ErrorCode::unknown_error, "fork() failed");
+std::vector<char*> argv;
+argv.reserve(command.size() + 1);
+for (const auto& arg : command) {
+    argv.push_back(const_cast<char*>(arg.c_str()));
 }
-if (pid == 0) {
-    setenv("GOGGLES_CAPTURE", "1", 1);
-    setenv("GOGGLES_WSI_PROXY", "1", 1);
-    setenv("DISPLAY", x11_display.c_str(), 1);
-    setenv("WAYLAND_DISPLAY", wayland_display.c_str(), 1);
+argv.push_back(nullptr);
 
-    if (app_width != 0 && app_height != 0) {
-        setenv("GOGGLES_WIDTH", std::to_string(app_width).c_str(), 1);
-        setenv("GOGGLES_HEIGHT", std::to_string(app_height).c_str(), 1);
-    }
+char* envp[] = {
+    const_cast<char*>("GOGGLES_CAPTURE=1"),
+    const_cast<char*>("GOGGLES_WSI_PROXY=1"),
+    const_cast<char*>(("DISPLAY=" + x11_display).c_str()),
+    const_cast<char*>(("WAYLAND_DISPLAY=" + wayland_display).c_str()),
+    nullptr
+};
 
-    std::vector<char*> argv;
-    argv.reserve(command.size() + 1);
-    for (const auto& arg : command) {
-        argv.push_back(const_cast<char*>(arg.c_str()));
-    }
-    argv.push_back(nullptr);
-
-    execvp(argv[0], argv.data());
-    _exit(127);
+pid_t pid;
+if (posix_spawnp(&pid, argv[0], nullptr, nullptr, argv.data(), envp) != 0) {
+    return goggles::make_error<pid_t>(goggles::ErrorCode::unknown_error, "posix_spawnp failed");
 }
 
+return pid;
+

[Suggestion processed]

Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies a critical thread-safety issue with using fork() in a potentially multi-threaded application and proposes using posix_spawnp as a safer alternative, preventing potential deadlocks.

High
Improve child process termination logic
Suggestion Impact:terminate_child was rewritten to poll waitpid(WNOHANG) with a deadline (using a helper lambda), first after sending SIGTERM and then escalating to SIGKILL with logging, preventing an indefinite waitpid() block.

code diff:

@@ -61,9 +90,48 @@
     if (pid <= 0) {
         return;
     }
-    kill(pid, SIGTERM);
-    int status = 0;
-    waitpid(pid, &status, 0);
+
+    auto reap_with_timeout = [](pid_t child_pid, std::chrono::milliseconds timeout) -> bool {
+        constexpr auto POLL_INTERVAL = std::chrono::milliseconds(50);
+        const auto deadline = std::chrono::steady_clock::now() + timeout;
+
+        for (;;) {
+            int status = 0;
+            const pid_t result = waitpid(child_pid, &status, WNOHANG);
+            if (result == child_pid) {
+                return true;
+            }
+            if (result == 0) {
+                if (std::chrono::steady_clock::now() >= deadline) {
+                    return false;
+                }
+                std::this_thread::sleep_for(POLL_INTERVAL);
+                continue;
+            }
+
+            if (errno == EINTR) {
+                continue;
+            }
+            // Child already reaped or not our child anymore.
+            return true;
+        }
+    };
+
+    constexpr auto SIGTERM_TIMEOUT = std::chrono::seconds(3);
+    constexpr auto SIGKILL_TIMEOUT = std::chrono::seconds(2);
+
+    (void)kill(pid, SIGTERM);
+    if (reap_with_timeout(pid, SIGTERM_TIMEOUT)) {
+        return;
+    }
+
+    GOGGLES_LOG_WARN("Target app did not exit after SIGTERM; sending SIGKILL (pid={})", pid);
+    (void)kill(pid, SIGKILL);
+    if (reap_with_timeout(pid, SIGKILL_TIMEOUT)) {
+        return;
+    }
+
+    GOGGLES_LOG_ERROR("Target app did not exit after SIGKILL (pid={})", pid);
 }

Modify terminate_child to avoid blocking indefinitely. Implement a timeout loop
after sending SIGTERM, and if the child process does not exit, forcefully
terminate it with SIGKILL.

src/app/main.cpp [60-67]

 static auto terminate_child(pid_t pid) -> void {
     if (pid <= 0) {
         return;
     }
-    kill(pid, SIGTERM);
-    int status = 0;
-    waitpid(pid, &status, 0);
+    if (kill(pid, SIGTERM) != 0) {
+        return; // Process may already be gone
+    }
+
+    // Wait up to 2 seconds for graceful termination
+    for (int i = 0; i < 200; ++i) {
+        int status = 0;
+        const pid_t result = waitpid(pid, &status, WNOHANG);
+        if (result == pid) {
+            return; // Child terminated
+        }
+        if (result < 0 && errno == ECHILD) {
+            return; // Child already reaped or does not exist
+        }
+        SDL_Delay(10); // Sleep for 10ms
+    }
+
+    GOGGLES_LOG_WARN("Child process (pid={}) did not exit after SIGTERM, sending SIGKILL", pid);
+    kill(pid, SIGKILL);
+    waitpid(pid, nullptr, 0);
 }

[Suggestion processed]

Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a potential hang in the terminate_child function and proposes a robust solution with a timeout and SIGKILL, significantly improving the application's shutdown reliability.

Medium
High-level
Separate launcher logic from viewer

The process launching and lifecycle management logic should be moved out of the
main viewer application and into a separate, dedicated launcher executable. This
would improve separation of concerns, adhering to the Single Responsibility
Principle.

Examples:

src/app/main.cpp [17-58]
static auto spawn_target_app(const std::vector<std::string>& command,
                             const std::string& x11_display, const std::string& wayland_display,
                             uint32_t app_width, uint32_t app_height) -> goggles::Result<pid_t> {
    if (command.empty()) {
        return goggles::make_error<pid_t>(goggles::ErrorCode::invalid_config,
                                          "missing target app command");
    }

    if (x11_display.empty() || wayland_display.empty()) {
        return goggles::make_error<pid_t>(goggles::ErrorCode::input_init_failed,

 ... (clipped 32 lines)
src/app/main.cpp [152-201]
        pid_t child_pid = -1;
        int child_status = 0;
        bool child_exited = false;

        if (!cli_opts.detach) {
            if (enable_input_forwarding) {
                const auto x11_display = app->x11_display();
                const auto wayland_display = app->wayland_display();

                auto spawn_result =

 ... (clipped 40 lines)

Solution Walkthrough:

Before:

// src/app/main.cpp
static auto run_app(int argc, char** argv) -> int {
    // ... parse CLI options
    // ... load config
    // ... create Application

    auto app = std::move(app_result.value());

    if (!cli_opts.detach) {
        // NEW: Launch and manage child process
        pid_t child_pid = spawn_target_app(...);
        while (app->is_running()) {
            app->pump_events();
            app->tick_frame();
            // NEW: check if child has exited
            waitpid(child_pid, ...);
        }
        // NEW: terminate child if viewer closes
        terminate_child(child_pid);
    } else {
        // OLD: Run viewer only
        app->run();
    }
    return EXIT_SUCCESS;
}

After:

// new file: src/launcher/main.cpp
int main(int argc, char** argv) {
    // Parse args to separate viewer args from app command
    
    // Start viewer in a new process
    // e.g., `goggles --detach --report-display-names-to-stdout`
    pid_t viewer_pid = fork_and_exec("goggles", viewer_args);

    // Read display names from viewer's stdout
    string x11_display, wayland_display;
    read_from_viewer_pipe(x11_display, wayland_display);

    // Start target app in a new process with correct env vars
    pid_t app_pid = fork_and_exec("app_command", app_args, env_vars);

    // Wait for either process to exit and terminate the other
    manage_process_lifecycles(viewer_pid, app_pid);
    return exit_code;
}

// src/app/main.cpp (simplified back)
static auto run_app(int argc, char** argv) -> int {
    // ... parse CLI, load config, create Application
    auto app = std::move(app_result.value());
    app->run(); // Always runs in viewer-only mode
    return EXIT_SUCCESS;
}
Suggestion importance[1-10]: 6

__

Why: The suggestion correctly identifies a valid architectural concern (Single Responsibility Principle), as the PR mixes process management logic into the main application, but it overlooks the practical reasons for this design, such as the need for the launcher to access runtime state from the viewer.

Low
General
Throttle event loop polling

Add a small delay (e.g., SDL_Delay(1)) inside the main event loop in main.cpp.
This will throttle the loop and prevent high CPU usage when the application is
idle.

src/app/main.cpp [174-185]

 while (app->is_running()) {
     app->pump_events();
     app->tick_frame();
 
     if (child_pid > 0 && !child_exited) {
         pid_t result = waitpid(child_pid, &child_status, WNOHANG);
         if (result == child_pid) {
             child_exited = true;
             push_quit_event();
         }
     }
+
+    SDL_Delay(1);  // throttle busy loop
 }
  • Apply / Chat
Suggestion importance[1-10]: 6

__

Why: The suggestion correctly identifies a busy-wait loop that can cause high CPU usage and proposes a simple, effective fix by adding a small delay, improving application performance.

Low
Log execvp failures
Suggestion Impact:The PR no longer uses execvp; it was refactored to use posix_spawnp instead. It adds explicit error reporting when posix_spawnp fails by returning an error message that includes strerror(rc), which serves the same purpose as logging execvp/perror failures (improves diagnosability of launch failures), albeit via a different mechanism.

code diff:

+    pid_t pid = -1;
+    const int rc = posix_spawnp(&pid, argv[0], nullptr, nullptr, argv.data(), envp.data());
+    if (rc != 0) {
+        return goggles::make_error<pid_t>(goggles::ErrorCode::unknown_error,
+                                          std::string("posix_spawnp() failed: ") +
+                                              std::strerror(rc));
     }

Add error logging using perror if execvp fails. This helps diagnose why a child
process could not be executed, such as a missing binary or incorrect path.

src/app/main.cpp [53-54]

 execvp(argv[0], argv.data());
+perror("execvp failed");
 _exit(127);

[Suggestion processed]

Suggestion importance[1-10]: 5

__

Why: This suggestion improves debuggability by logging the reason for an execvp failure, which is helpful for diagnosing issues with launching the target application.

Low
  • Update

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @openspec/changes/update-launch-modes-default-detach/proposal.md:
- Around line 9-21: This proposal introduces breaking CLI behavior (new default
vs. detach modes, enforced input forwarding, rejected options like
--app-width/--app-height and implicit GOGGLES_WSI_PROXY), so update the "What
Changes" section to explicitly mark these items with **BREAKING**; specifically
annotate that default mode changes CLI semantics (requirement of `--` separator,
implicit `GOGGLES_WSI_PROXY=1`, setting of `DISPLAY`/`WAYLAND_DISPLAY`, and
support for `--app-width`/`--app-height`) and that detach mode rejects those
options (e.g., `--app-width`, `--app-height`, input forwarding disabled), and
confirm config interaction remains via `config/goggles.toml` with `--config`
overrides.
🧹 Nitpick comments (2)
src/app/main.cpp (2)

60-67: Consider adding a timeout before escalating to SIGKILL.

If the child process ignores SIGTERM, waitpid will block indefinitely. A robust pattern would wait with a timeout and escalate to SIGKILL if needed.

♻️ Optional: Add timeout-based escalation
 static auto terminate_child(pid_t pid) -> void {
     if (pid <= 0) {
         return;
     }
     kill(pid, SIGTERM);
-    int status = 0;
-    waitpid(pid, &status, 0);
+    int status = 0;
+    for (int i = 0; i < 50; ++i) { // 5 second timeout
+        if (waitpid(pid, &status, WNOHANG) == pid) {
+            return;
+        }
+        usleep(100000); // 100ms
+    }
+    kill(pid, SIGKILL);
+    waitpid(pid, &status, 0);
 }

156-172: Redundant condition check.

The condition if (enable_input_forwarding) on line 157 is always true in default mode since enable_input_forwarding = !cli_opts.detach (line 134), and we're already inside the if (!cli_opts.detach) block. This nested check can be removed to simplify the logic.

♻️ Simplify by removing redundant condition
         if (!cli_opts.detach) {
-            if (enable_input_forwarding) {
-                const auto x11_display = app->x11_display();
-                const auto wayland_display = app->wayland_display();
-
-                auto spawn_result =
-                    spawn_target_app(cli_opts.app_command, x11_display, wayland_display,
-                                     cli_opts.app_width, cli_opts.app_height);
-                if (!spawn_result) {
-                    GOGGLES_LOG_CRITICAL("Failed to launch target app: {} ({})",
-                                         spawn_result.error().message,
-                                         goggles::error_code_name(spawn_result.error().code));
-                    return EXIT_FAILURE;
-                }
-                child_pid = spawn_result.value();
-                GOGGLES_LOG_INFO("Launched target app (pid={})", child_pid);
+            const auto x11_display = app->x11_display();
+            const auto wayland_display = app->wayland_display();
+
+            auto spawn_result =
+                spawn_target_app(cli_opts.app_command, x11_display, wayland_display,
+                                 cli_opts.app_width, cli_opts.app_height);
+            if (!spawn_result) {
+                GOGGLES_LOG_CRITICAL("Failed to launch target app: {} ({})",
+                                     spawn_result.error().message,
+                                     goggles::error_code_name(spawn_result.error().code));
+                return EXIT_FAILURE;
             }
+            child_pid = spawn_result.value();
+            GOGGLES_LOG_INFO("Launched target app (pid={})", child_pid);
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1b5bbb2 and 188eac1.

📒 Files selected for processing (14)
  • README.md
  • config/goggles.toml
  • docs/input_forwarding.md
  • openspec/changes/update-launch-modes-default-detach/design.md
  • openspec/changes/update-launch-modes-default-detach/proposal.md
  • openspec/changes/update-launch-modes-default-detach/specs/app-window/spec.md
  • openspec/changes/update-launch-modes-default-detach/tasks.md
  • scripts/task/start.sh
  • src/app/application.cpp
  • src/app/application.hpp
  • src/app/cli.hpp
  • src/app/main.cpp
  • tests/CMakeLists.txt
  • tests/app/test_cli.cpp
🧰 Additional context used
📓 Path-based instructions (3)
openspec/**/*.md

📄 CodeRabbit inference engine (openspec/AGENTS.md)

openspec/**/*.md: Use #### Scenario: format (4 hashtags) for scenario headers in requirements, not bullets or bold text, with at least one scenario per requirement
Use format - **WHEN** [condition] - **THEN** [expected result] for scenario steps in requirements

Files:

  • openspec/changes/update-launch-modes-default-detach/proposal.md
  • openspec/changes/update-launch-modes-default-detach/specs/app-window/spec.md
  • openspec/changes/update-launch-modes-default-detach/tasks.md
  • openspec/changes/update-launch-modes-default-detach/design.md
openspec/changes/**/proposal.md

📄 CodeRabbit inference engine (openspec/AGENTS.md)

List spec deltas in proposal.md under 'What Changes' section, marking breaking changes with BREAKING

Files:

  • openspec/changes/update-launch-modes-default-detach/proposal.md
openspec/changes/**/specs/**/*.md

📄 CodeRabbit inference engine (openspec/AGENTS.md)

openspec/changes/**/specs/**/*.md: Write spec deltas using ## ADDED|MODIFIED|REMOVED|RENAMED Requirements format with at least one #### Scenario: per requirement in spec files
When modifying existing requirements in a MODIFIED delta, paste the full requirement block including the header and all scenarios, as the archiver will replace the entire requirement
Use ADDED for new capabilities that can stand alone; use MODIFIED for changes to existing requirement behavior, scope, or acceptance criteria; use RENAMED for name-only changes

Files:

  • openspec/changes/update-launch-modes-default-detach/specs/app-window/spec.md
🧠 Learnings (15)
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/tests/**/*.cpp : Test organization: Directory structure mirrors `src/`. Test files named `test_<module>.cpp`. Test cases use descriptive names and sections for organization.

Applied to files:

  • tests/CMakeLists.txt
  • tests/app/test_cli.cpp
📚 Learning: 2025-12-29T15:44:30.520Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/project.md:0-0
Timestamp: 2025-12-29T15:44:30.520Z
Learning: Applies to openspec/tests/**/*.{cpp,hpp} : Focus unit tests on non-GPU logic: utility functions, configuration parsing, and pipeline graph logic

Applied to files:

  • tests/CMakeLists.txt
  • tests/app/test_cli.cpp
📚 Learning: 2025-12-29T15:44:30.520Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/project.md:0-0
Timestamp: 2025-12-29T15:44:30.520Z
Learning: Applies to openspec/tests/**/*.{cpp,hpp} : Use Catch2 v3 for unit testing with test files mirroring the `src/` directory structure

Applied to files:

  • tests/CMakeLists.txt
📚 Learning: 2025-12-19T08:54:18.412Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/AGENTS.md:0-0
Timestamp: 2025-12-19T08:54:18.412Z
Learning: Applies to openspec/changes/**/specs/**/*.md : Use ADDED for new capabilities that can stand alone; use MODIFIED for changes to existing requirement behavior, scope, or acceptance criteria; use RENAMED for name-only changes

Applied to files:

  • openspec/changes/update-launch-modes-default-detach/specs/app-window/spec.md
  • openspec/changes/update-launch-modes-default-detach/tasks.md
📚 Learning: 2026-01-07T07:20:57.053Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.053Z
Learning: Applies to docs/**/*.hpp : Define a lightweight `Error` struct with `ErrorCode` enum, `std::string message`, and optional `std::source_location loc` in the `goggles` namespace.

Applied to files:

  • src/app/application.hpp
📚 Learning: 2026-01-07T07:20:57.053Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.053Z
Learning: Applies to docs/**/*.{cpp,hpp} : Use top-level namespace `goggles`. Use module namespaces `goggles::<module>` (e.g., `goggles::capture`, `goggles::render`). Use sub-module namespaces `goggles::<module>::<submodule>`. Never use `using namespace` in headers. Prefer explicit namespace qualification or scoped `using` declarations.

Applied to files:

  • src/app/application.hpp
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/**/*.cpp : Error handling macros: Use `VK_TRY` for vk::Result early return in Vulkan code. Use `GOGGLES_TRY` for Result<T> propagation (both as statement and expression). Use `GOGGLES_MUST` for internal invariants that should never fail. Use manual pattern for destructors/cleanup functions where propagation is impossible.

Applied to files:

  • src/app/application.hpp
📚 Learning: 2025-12-29T15:44:30.520Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/project.md:0-0
Timestamp: 2025-12-29T15:44:30.520Z
Learning: Applies to openspec/**/*.hpp : Organize namespaces as `goggles` with sub-modules like `goggles::capture`, `goggles::render`; never use `using namespace` in headers

Applied to files:

  • src/app/application.hpp
📚 Learning: 2025-12-29T15:44:30.520Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/project.md:0-0
Timestamp: 2025-12-29T15:44:30.520Z
Learning: Applies to openspec/src/**/*.{cpp,hpp} : Use inter-thread communication via `goggles::util::SPSCQueue` (lock-free SPSC queue) for task parallelism

Applied to files:

  • src/app/application.hpp
📚 Learning: 2026-01-07T07:20:57.053Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.053Z
Learning: Applies to docs/**/*.{cpp,hpp} : Use standard spdlog severity levels: trace, debug, info, warn, error, critical. Define project-specific logging macros (`GOGGLES_LOG_TRACE`, `GOGGLES_LOG_DEBUG`, etc.) wrapping spdlog.

Applied to files:

  • src/app/application.hpp
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/render/**/*.cpp : Main thread owns: Vulkan instance, device, swapchain lifecycle; queue submission; window events and user input; job coordination. Main thread MUST NOT: block on I/O, perform heavy computation (>1ms), allocate memory in per-frame code paths.

Applied to files:

  • README.md
  • src/app/main.cpp
📚 Learning: 2025-12-29T15:44:30.520Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/project.md:0-0
Timestamp: 2025-12-29T15:44:30.520Z
Learning: The shader pipeline is a multi-pass model designed for compatibility with RetroArch shader semantics

Applied to files:

  • README.md
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/render/**/*.cpp : Single-threaded render loop on main thread by default. Render backend (`goggles::render`) runs on main thread. Pipeline execution runs on main thread. Job system for non-render work on worker threads. Threading introduced only when profiling justifies it (main thread CPU consistently >8ms).

Applied to files:

  • README.md
  • src/app/main.cpp
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/**/*.toml : Use TOML format for all configuration files. Use `toml11` or `tomlplusplus` library. No JSON (no comments) or YAML (complex, ambiguous). Development/testing uses `config/goggles.toml` in repository root.

Applied to files:

  • config/goggles.toml
📚 Learning: 2025-12-29T15:44:30.520Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/project.md:0-0
Timestamp: 2025-12-29T15:44:30.520Z
Learning: Support RetroArch shader format for wide compatibility

Applied to files:

  • config/goggles.toml
🧬 Code graph analysis (3)
src/app/main.cpp (5)
src/app/application.cpp (4)
  • x11_display (112-117)
  • x11_display (112-112)
  • wayland_display (119-124)
  • wayland_display (119-119)
src/input/input_forwarder.cpp (4)
  • x11_display (216-218)
  • x11_display (216-216)
  • wayland_display (220-222)
  • wayland_display (220-220)
src/input/compositor_server.cpp (4)
  • x11_display (152-157)
  • x11_display (152-152)
  • wayland_display (159-161)
  • wayland_display (159-159)
src/util/error.hpp (2)
  • make_error (50-52)
  • error_code_name (68-68)
src/app/cli.hpp (2)
  • arg (76-76)
  • parse_cli (25-25)
src/app/cli.hpp (2)
src/app/application.hpp (1)
  • nodiscard (46-46)
src/util/error.hpp (5)
  • nodiscard (50-54)
  • nodiscard (57-59)
  • nodiscard (62-66)
  • nodiscard (68-104)
  • make_error (50-52)
tests/app/test_cli.cpp (1)
src/app/cli.hpp (3)
  • arg (76-76)
  • nodiscard (25-149)
  • parse_cli (25-25)
🪛 LanguageTool
openspec/changes/update-launch-modes-default-detach/proposal.md

[style] ~5-~5: To elevate your writing, try using a synonym here.
Context: ...WSI proxy sizing). This is error-prone, hard to remember, and not suitable for packa...

(HARD_TO)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build and Test (test preset)
  • GitHub Check: Static Analysis (clang-tidy)
🔇 Additional comments (26)
config/goggles.toml (1)

28-28: LGTM! Shader preset updated.

The shader preset change from crt-royale to zfast-crt is a straightforward configuration update. Both are valid RetroArch CRT shaders, and this change doesn't affect other configuration settings.

tests/CMakeLists.txt (1)

16-18: LGTM! Test file added following project conventions.

The addition of app/test_cli.cpp correctly follows the project's test organization structure (mirroring src/ directory layout) and integrates properly with the existing CMake test configuration.

src/app/application.cpp (1)

112-124: LGTM! Safe accessor implementations with proper null checks.

Both x11_display() and wayland_display() correctly check for null m_input_forwarder before delegating, returning an empty string when input forwarding is disabled. This is a safe and reasonable design that prevents null pointer dereferences while providing a sensible default value.

src/app/application.hpp (2)

4-4: LGTM! Required header added.

The <string> header is correctly added to support the new std::string return types for the display accessor methods.


47-48: LGTM! Well-designed accessor declarations.

Both accessor methods are properly marked:

  • [[nodiscard]] encourages callers to use the return value
  • const correctness ensures no state modification
  • Return type std::string is appropriate for display identifiers
docs/input_forwarding.md (2)

161-162: LGTM! Documentation updated to reflect preferred workflow.

The note correctly emphasizes the new default mode behavior where Goggles automatically manages the nested compositor and environment variables. This provides clear guidance for users while maintaining the manual launch instructions for reference.


179-181: LGTM! Test commands simplified for default launch mode.

The updated test commands correctly demonstrate the single-command workflow (goggles -- <test_app>), which aligns with the new default mode behavior and simplifies the testing process compared to the previous two-terminal procedure.

openspec/changes/update-launch-modes-default-detach/design.md (1)

1-43: LGTM!

The design document clearly defines the two launch modes, their behavior, config interaction, and hard CLI requirements. The mode definitions and dependency rules provide solid foundations for the implementation.

scripts/task/start.sh (2)

36-56: Clear usage documentation for new launch modes.

The updated usage text effectively communicates the two modes (default with app launch, detach for viewer-only) and includes helpful examples. The requirement for the -- separator is clearly stated.


105-127: Simplified argument forwarding is correct.

The refactored script now collects all remaining arguments and forwards them directly to the viewer binary, removing the complex environment variable and lifecycle management that has been moved to the main application. This simplification improves maintainability while maintaining correct behavior.

tests/app/test_cli.cpp (2)

11-23: Well-designed test utility.

The ArgvBuilder helper cleanly constructs argc/argv pairs for testing, improving test readability and reducing boilerplate.


31-99: Comprehensive CLI parsing test coverage.

The test suite effectively covers key scenarios:

  • Detach mode validation (no app command, rejection of width/height)
  • Default mode requirements (separator, app command)
  • App argument isolation (options after -- don't interfere)
  • Width/height pairing validation

The test organization and naming align with project conventions.

README.md (2)

51-65: Clear usage documentation with practical examples.

The updated usage section effectively explains the new launch modes with concrete examples. The separator requirement is clearly stated, and both default and detach modes are demonstrated. The examples progress from simple to more complex (with sizing options), which aids understanding.


67-71: Helpful lifecycle and integration notes.

The note about exit behavior (viewer exits when app exits, and vice versa) clarifies the process coordination model. The Steam integration guidance is practical, with appropriate mention that full packaging support is planned.

openspec/changes/update-launch-modes-default-detach/tasks.md (1)

1-34: LGTM!

The task checklist is well-organized with clear sections and appropriate completion status. The pending verification items (4.1-4.3) correctly remain unchecked as manual testing items.

openspec/changes/update-launch-modes-default-detach/specs/app-window/spec.md (1)

1-38: LGTM!

The spec file correctly uses the ## MODIFIED Requirements and ## ADDED Requirements format with #### Scenario: headers for each requirement. The BDD-style GIVEN/WHEN/THEN structure is clear and well-organized.

src/app/main.cpp (5)

17-58: LGTM!

The spawn_target_app function correctly handles:

  • Input validation for command and display info
  • Fork error handling
  • Environment variable setup in child process
  • Proper use of _exit(127) after failed execvp to avoid flushing parent's stdio buffers

69-73: LGTM!

Clean helper to push a quit event to the SDL event queue for coordinated shutdown.


75-85: LGTM!

Good error handling that distinguishes between help/version requests (success) and actual parsing errors (failure with guidance).


134-140: LGTM!

Clear derivation of enable_input_forwarding from the detach flag with informative logging when mode overrides config.


199-205: LGTM!

The detach mode path correctly uses app->run() for the simple viewer-only loop, with appropriate shutdown logging.

src/app/cli.hpp (5)

13-21: LGTM!

The CliOptions struct is cleanly extended with the new fields for detach mode and app configuration.


28-59: LGTM!

Good CLI documentation with clear usage examples, deprecation notice for --input-forwarding, and sensible range validation (1-16384) for dimension options.


61-69: LGTM!

Clean separator detection logic that correctly partitions viewer options from app command.


71-100: LGTM!

The exception handling correctly distinguishes between help/version requests and actual parsing errors, with appropriate error messages for missing separator scenarios.


102-148: LGTM!

Thorough post-parse validation that correctly enforces:

  • Detach mode incompatibilities (--input-forwarding, --app-width/--app-height, app command)
  • Default mode requirements (separator, app command)
  • Width/height pair validation

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (5)
src/app/main.cpp (5)

31-34: Consider more specific error messaging.

The error message "input forwarding display information unavailable" might be misleading. This condition occurs when the Application's display accessors return empty strings, which could happen if:

  • Input forwarding failed to initialize
  • The compositor server hasn't set up displays yet
  • Input forwarding was disabled

Consider making the error message more specific to aid debugging, such as: "Display information not available (x11_display or wayland_display is empty)."


48-51: Validate width/height consistency.

The code sets GOGGLES_WIDTH and GOGGLES_HEIGHT only when both are non-zero, but doesn't validate or warn when exactly one dimension is zero (e.g., width=1920, height=0). This silent acceptance of inconsistent dimensions might cause unexpected behavior.

Consider validating that both dimensions are zero or both are non-zero, and return an error for invalid combinations.

🛡️ Proposed validation
 
-    if (app_width != 0 && app_height != 0) {
+    if (app_width != 0 || app_height != 0) {
+        if (app_width == 0 || app_height == 0) {
+            return goggles::make_error<pid_t>(goggles::ErrorCode::invalid_config,
+                                              "app_width and app_height must both be zero or both non-zero");
+        }
         env_overrides.emplace_back("GOGGLES_WIDTH=" + std::to_string(app_width));
         env_overrides.emplace_back("GOGGLES_HEIGHT=" + std::to_string(app_height));
     }

71-76: Document const_cast safety.

The const_cast<char*> on line 74 is required by the POSIX posix_spawnp API, which takes char* const argv[] but doesn't modify the strings. While this is safe in practice (the strings come from the command vector and won't be modified), it's worth adding a brief comment to document this requirement and reassure future maintainers.

📝 Suggested comment
     std::vector<char*> argv;
     argv.reserve(command.size() + 1);
     for (const auto& arg : command) {
+        // const_cast required for POSIX API; posix_spawnp doesn't modify argv
         argv.push_back(const_cast<char*>(arg.c_str()));
     }
     argv.push_back(nullptr);

123-132: Consider checking kill() return values.

The return values of kill() calls are currently ignored. While this might be acceptable for robustness, checking for ESRCH (no such process) could provide useful diagnostic information and avoid unnecessary waiting.

🔍 Proposed improvement
-    (void)kill(pid, SIGTERM);
+    if (kill(pid, SIGTERM) != 0 && errno == ESRCH) {
+        // Process already exited
+        return;
+    }
     if (reap_with_timeout(pid, SIGTERM_TIMEOUT)) {
         return;
     }
 
     GOGGLES_LOG_WARN("Target app did not exit after SIGTERM; sending SIGKILL (pid={})", pid);
-    (void)kill(pid, SIGKILL);
+    if (kill(pid, SIGKILL) != 0 && errno == ESRCH) {
+        // Process already exited
+        return;
+    }
     if (reap_with_timeout(pid, SIGKILL_TIMEOUT)) {
         return;
     }

224-240: Redundant condition check.

On line 225, the check if (enable_input_forwarding) is redundant. Since enable_input_forwarding is set to !cli_opts.detach on line 202, and this entire block is guarded by if (!cli_opts.detach) on line 224, the condition will always be true.

Consider removing the inner condition or adding a comment explaining if there's a future scenario where these might differ.

♻️ Simplified code
         if (!cli_opts.detach) {
-            if (enable_input_forwarding) {
-                const auto x11_display = app->x11_display();
-                const auto wayland_display = app->wayland_display();
-
-                auto spawn_result =
-                    spawn_target_app(cli_opts.app_command, x11_display, wayland_display,
-                                     cli_opts.app_width, cli_opts.app_height);
-                if (!spawn_result) {
-                    GOGGLES_LOG_CRITICAL("Failed to launch target app: {} ({})",
-                                         spawn_result.error().message,
-                                         goggles::error_code_name(spawn_result.error().code));
-                    return EXIT_FAILURE;
-                }
-                child_pid = spawn_result.value();
-                GOGGLES_LOG_INFO("Launched target app (pid={})", child_pid);
+            const auto x11_display = app->x11_display();
+            const auto wayland_display = app->wayland_display();
+
+            auto spawn_result =
+                spawn_target_app(cli_opts.app_command, x11_display, wayland_display,
+                                 cli_opts.app_width, cli_opts.app_height);
+            if (!spawn_result) {
+                GOGGLES_LOG_CRITICAL("Failed to launch target app: {} ({})",
+                                     spawn_result.error().message,
+                                     goggles::error_code_name(spawn_result.error().code));
+                return EXIT_FAILURE;
             }
+            child_pid = spawn_result.value();
+            GOGGLES_LOG_INFO("Launched target app (pid={})", child_pid);
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 188eac1 and 7801725.

📒 Files selected for processing (1)
  • src/app/main.cpp
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/render/**/*.cpp : Single-threaded render loop on main thread by default. Render backend (`goggles::render`) runs on main thread. Pipeline execution runs on main thread. Job system for non-render work on worker threads. Threading introduced only when profiling justifies it (main thread CPU consistently >8ms).

Applied to files:

  • src/app/main.cpp
🧬 Code graph analysis (1)
src/app/main.cpp (6)
src/app/application.cpp (4)
  • x11_display (112-117)
  • x11_display (112-112)
  • wayland_display (119-124)
  • wayland_display (119-119)
src/input/input_forwarder.cpp (4)
  • x11_display (216-218)
  • x11_display (216-216)
  • wayland_display (220-222)
  • wayland_display (220-220)
src/input/compositor_server.cpp (4)
  • x11_display (152-157)
  • x11_display (152-152)
  • wayland_display (159-161)
  • wayland_display (159-159)
src/util/config.hpp (1)
  • to_string (19-19)
src/app/cli.hpp (1)
  • arg (76-76)
src/app/application.hpp (1)
  • config (29-30)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Static Analysis (clang-tidy)
  • GitHub Check: Build and Test (test preset)
🔇 Additional comments (5)
src/app/main.cpp (5)

4-21: LGTM!

The added includes are appropriate for the process spawning, signal handling, and child process monitoring functionality introduced in this PR.


137-141: LGTM!

The SDL quit event push implementation is straightforward and appropriate for signaling viewer shutdown when the child process exits.


145-153: LGTM!

The improved CLI error handling provides better user feedback with clear error messages and usage hints, which enhances the user experience.


202-208: LGTM!

The input forwarding logic correctly implements the default/detach mode behavior, and the informative logging helps users understand when the mode overrides the configuration.


255-259: Verify exit status semantics when viewer closes first.

When the viewer exits before the target app (e.g., user closes the window), the code terminates the child and returns EXIT_FAILURE. This might not accurately represent all scenarios—if a user intentionally closes the viewer, should this be treated as a failure?

Consider whether:

  • EXIT_SUCCESS might be more appropriate for user-initiated viewer closure
  • A different exit code could distinguish "viewer closed first" from "child failed to launch"
  • The current behavior is intentional to indicate the child was forcibly terminated

The current implementation ensures the child is cleaned up, which is correct for resource management.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant