{"id":30237018,"url":"https://github.com/philiplinden/bevy_repl","last_synced_at":"2025-08-15T01:35:56.974Z","repository":{"id":307074614,"uuid":"1026924974","full_name":"philiplinden/bevy_repl","owner":"philiplinden","description":"Add a REPL to a Bevy app","archived":false,"fork":false,"pushed_at":"2025-08-14T03:25:47.000Z","size":2758,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-08-14T05:27:20.825Z","etag":null,"topics":["bevy","bevy-plugin","cli","developer-tools","repl"],"latest_commit_sha":null,"homepage":"https://crates.io/crates/bevy_repl","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/philiplinden.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE-APACHE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null},"funding":{"ko_fi":"philiplinden"}},"created_at":"2025-07-26T23:14:47.000Z","updated_at":"2025-08-14T03:18:31.000Z","dependencies_parsed_at":"2025-07-29T10:36:10.781Z","dependency_job_id":"d3116162-284b-4760-9a3f-94b441974c43","html_url":"https://github.com/philiplinden/bevy_repl","commit_stats":null,"previous_names":["philiplinden/bevy_repl"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/philiplinden/bevy_repl","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/philiplinden%2Fbevy_repl","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/philiplinden%2Fbevy_repl/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/philiplinden%2Fbevy_repl/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/philiplinden%2Fbevy_repl/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/philiplinden","download_url":"https://codeload.github.com/philiplinden/bevy_repl/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/philiplinden%2Fbevy_repl/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":270510980,"owners_count":24597653,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-08-14T02:00:10.309Z","response_time":75,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bevy","bevy-plugin","cli","developer-tools","repl"],"created_at":"2025-08-15T01:35:54.103Z","updated_at":"2025-08-15T01:35:56.964Z","avatar_url":"https://github.com/philiplinden.png","language":"Rust","funding_links":["https://ko-fi.com/philiplinden"],"categories":[],"sub_categories":[],"readme":"# bevy_repl\n\n![Made with VHS](https://vhs.charm.sh/vhs-6kUt4mnyvUcmbpVfWzHx4s.gif)\n\nAn interactive REPL for headless Bevy apps powered by `clap` for command parsing\nand `bevy_ratatui` for terminal input and output. The plugin adds a text input\narea below the terminal output for interaction even in headless mode.\n\n- Unobtrusive TUI console below normal terminal output\n- Command parsing and CLI features from `clap`  \n- Observer-based command execution system with full Bevy ECS access for both\n  read and write operations\n- Logging integration with `bevy_log` and `tracing` for unified output display\n- Support for custom prompt rendering and minimal prompt mode\n- Works in tandem with windowed apps from the terminal\n- Built-in commands for common tasks (just `quit` for now)\n\nThe REPL is designed as an alternative to\n[makspll/bevy-console](https://github.com/makspll/bevy-console) for Bevy apps\nthat want a terminal-like console to modify the game at runtime without\nimplementing a full TUI or rendering features.\n\n\u003e This is my first public Bevy plugin, and I vibe-coded a large part\n\u003e of it. **You have been warned.**\n\n## Table of Contents\n\n- [bevy\\_repl](#bevy_repl)\n  - [Table of Contents](#table-of-contents)\n  - [Features](#features)\n    - [Derive](#derive)\n    - [Built-in commands](#built-in-commands)\n    - [Prompt styling](#prompt-styling)\n      - [Custom renderer (feature-gated: `pretty`)](#custom-renderer-feature-gated-pretty)\n      - [Plugin groups and alternate screen](#plugin-groups-and-alternate-screen)\n    - [Robust printing in raw/alternate screen terminals](#robust-printing-in-rawalternate-screen-terminals)\n    - [Routing Bevy logs to the REPL](#routing-bevy-logs-to-the-repl)\n    - [Startup ordering (PostStartup)](#startup-ordering-poststartup)\n  - [Usage](#usage)\n    - [REPL lifecycle (v1)](#repl-lifecycle-v1)\n    - [Builder pattern (default)](#builder-pattern-default)\n    - [Derive pattern (requires `derive` feature)](#derive-pattern-requires-derive-feature)\n    - [Prompt styling](#prompt-styling-1)\n    - [Default keybinds](#default-keybinds)\n  - [Design](#design)\n    - [Headless mode](#headless-mode)\n    - [REPL Console](#repl-console)\n    - [Command parsing](#command-parsing)\n    - [Scheduling](#scheduling)\n    - [Directly modifying the terminal (to-do)](#directly-modifying-the-terminal-to-do)\n    - [Prompt styling](#prompt-styling-2)\n  - [Known issues \\\u0026 limitations](#known-issues--limitations)\n    - [Built-in `help` and `clear` commands are not yet implemented](#built-in-help-and-clear-commands-are-not-yet-implemented)\n    - [Runtime toggle is not supported](#runtime-toggle-is-not-supported)\n    - [Key events are not forwarded to Bevy](#key-events-are-not-forwarded-to-bevy)\n    - [Minimal renderer prompt does not scroll with terminal output](#minimal-renderer-prompt-does-not-scroll-with-terminal-output)\n    - [Pretty renderer log history doesn't scroll at all](#pretty-renderer-log-history-doesnt-scroll-at-all)\n    - [Shift+ aren't entered into the buffer](#shift-arent-entered-into-the-buffer)\n  - [Aspirations](#aspirations)\n  - [License](#license)\n\n## Features\n\nTheoretically all clap features are supported, but I have only tested `derive`.\nOverride the `clap` features in your `Cargo.toml` to enable or disable\nadditional features at your own risk.\n\n### Derive\nUse the `derive` feature to support clap's derive pattern for REPL commands.\n`#[derive(ReplCommand)]` will automatically implement the `ReplCommand` trait\nand create an event with the command's arguments and options. Configure the\nresponse by adding an observer for the REPL command like normal.\n\n### Built-in commands\nEnable built-in commands with feature flags. Each command is enabled separately\nby a feature flag. Use the `default_commands` feature to enable all built-in\ncommands.\n\n| Feature Flag | Command | Description |\n| --- | --- | --- |\n| `default_commands` | `quit`, `help`, `clear` | Enable all built-in commands |\n| `quit` | `quit`, `q`, `exit` | Gracefully terminate the application |\n| `help` | `help` | Show clap help text (not yet implemented) |\n| `clear` | `clear` | Clear the terminal output |\n\n### Prompt styling\n\nThe prompt can be styled with the `pretty` feature. The feature adds a border,\ncolorful styles for title/prompt/hints, and a right-aligned hint text.\n\n- __Minimal (default)__\n  - Appearance: 1-line bottom prompt with symbol + input. No border/colors/hint.\n  - Compilation: no styling code compiled; lean terminal manipulation only.\n  - Config: only `ReplPromptConfig.symbol` is honored.\n  - Use: `cargo run` (no extra feature flags).\n\n- __Pretty (`--features pretty`)__\n  - Appearance: border with title, colored styles, right-aligned usage hint.\n  - Compilation: styling code compiled and enabled.\n  - Config: presets or explicit `ReplPromptConfig { symbol, border, color, hint }`.\n  - Use `ReplPlugins.set(PromptPlugin::pretty())` as shown below.\n\n#### Custom renderer (feature-gated: `pretty`)\n\nYou can swap the prompt renderer at runtime by overriding the `ActiveRenderer` resource\nwith your own implementation of the `PromptRenderer` trait. This is the recommended\nextension point for custom styles.\n\n- Build and run the demo custom renderer example:\n\n  ```bash\n  cargo run --example custom_renderer --features pretty\n  ```\n\n- Minimal usage (in your Bevy app):\n\n  ```rust\n  use bevy_repl::prompt::renderer::{PromptRenderer, RenderCtx};\n\n  struct MyRenderer;\n  impl PromptRenderer for MyRenderer {\n      fn render(\u0026self, f: \u0026mut ratatui::Frame\u003c'_\u003e, ctx: \u0026RenderCtx) {\n          // draw a simple 1-line prompt in your own style\n          // (see examples/custom_renderer.rs for a complete reference)\n          let area = bevy_repl::prompt::renderer::helpers::bottom_bar_area(ctx.area, 1);\n          let prompt = ctx.prompt.symbol.clone().unwrap_or_default();\n          let spans = [ratatui::text::Span::raw(prompt), ratatui::text::Span::raw(\u0026ctx.repl.buffer)];\n          f.render_widget(ratatui::widgets::Paragraph::new(ratatui::text::Line::from(spans)), area);\n      }\n  }\n\n  App::new()\n      .add_plugins(ReplPlugins.set(PromptPlugin {\n        renderer: MyRenderer,\n        ..default()\n      }))\n      .run();\n  ```\n\nThe example and docs assume the `pretty` feature is enabled so the rendering\ninfrastructure is available. Custom renderers can ignore colors/borders entirely\nif you want a minimal look.\n\n#### Plugin groups and alternate screen\n\n- __When is the alternate screen active?__\n  - The alternate screen is active when `bevy_ratatui::RatatuiPlugins` is added to your app.\n  - Using `ReplPlugins` (the default/turnkey group) automatically adds `RatatuiPlugins`, so the REPL renders in the alternate screen via `RatatuiContext`.\n  - Using `MinimalReplPlugins` adds some but not all Ratatui Plugins; the prompt renders on the main terminal screen using the fallback `FallbackTerminalContext`.\n\n- __Minimal (no alternate screen, no built-ins)__\n\n  ```rust\n  use bevy::{app::ScheduleRunnerPlugin, prelude::*};\n  use bevy_repl::plugin::MinimalReplPlugins;\n  use std::time::Duration;\n\n  fn main() {\n      App::new()\n          .add_plugins((\n              MinimalPlugins.set(ScheduleRunnerPlugin::run_loop(Duration::from_secs_f64(1.0/60.0))),\n              // Minimal REPL: core + prompt + parser; main-screen rendering\n              MinimalReplPlugins,\n          ))\n          // Add your own commands (no built-ins in minimal)\n          // .add_repl_command::\u003cYourCommand\u003e()\n          // .add_observer(on_your_command)\n          .run();\n  }\n  ```\n\n- __Default/turnkey (alternate screen + built-ins)__\n\n  ```rust\n  use bevy::{app::ScheduleRunnerPlugin, prelude::*};\n  use bevy_repl::plugin::ReplPlugins;\n  use std::time::Duration;\n\n  fn main() {\n      App::new()\n          .add_plugins((\n              MinimalPlugins.set(ScheduleRunnerPlugin::run_loop(Duration::from_secs_f64(1.0/60.0))),\n              // Default REPL: adds RatatuiPlugins + minimal stack + built-ins\n              ReplPlugins,\n          ))\n          .run();\n  }\n  ```\n\n- __How to choose__\n  - Choose `MinimalReplPlugins` if you:\n    - Want to stay on the main terminal screen (no full TUI/pane UX).\n    - Intend to manage `bevy_ratatui` or other input/render stacks yourself.\n    - Prefer to opt-in to commands individually (no built-ins by default).\n  - Choose `ReplPlugins` if you:\n    - Want a turnkey setup with reliable prompt rendering in the alternate screen.\n    - Prefer sane defaults including built-in commands (`quit`, `help`, `clear`).\n    - Don’t need to wire `RatatuiPlugins` manually.\n\n  By default `ReplPlugins` uses the minimal prompt renderer. To enable the pretty renderer when the `pretty` feature is on, use `ReplPlugins.set(PromptPlugin::pretty())`.\n\n### Robust printing in raw/alternate screen terminals\n\nWhen the REPL is active, the terminal often runs in raw mode and may use the alternate screen. In these contexts, normal `println!` can leave the cursor in an odd position or produce inconsistent newlines. To ensure safe, consistent output, use the provided `bevy_repl::repl_println!` macro instead of `println!`.\n\n- __What it does__\n  - Minimal renderer: moves the cursor to column 0 before printing, writes CRLF (`\\r\\n`), and flushes stdout.\n  - Pretty renderer: additionally cooperates with the terminal scroll region reserved for the prompt; before printing it moves to the last scrollable line so output scrolls above the prompt without overwriting it.\n\n- __When to use__\n  - Any time you print from systems/observers while the REPL is active\n  - Especially in raw mode or when using the alternate screen (e.g., with `ReplPlugins`)\n\n- __Example__\n\n```rust\nfn on_ping(_trigger: Trigger\u003cPingCommand\u003e) {\n    bevy_repl::repl_println!(\"Pong\");\n}\n\nfn instructions() {\n    bevy_repl::repl_println!();\n    bevy_repl::repl_println!(\"Welcome to the Bevy REPL!\");\n}\n```\n\nIf you truly need to emit raw `stdout` (e.g., piping to tools) while the REPL is active, consider temporarily suspending the TUI or buffering output and emitting it via `repl_println!`.\n\n### Routing Bevy logs to the REPL\n\nYou can route logs produced by Bevy's `tracing` pipeline to the REPL so they appear above the prompt and scroll correctly.\n\n- __How it works__\n  - A custom `tracing` Layer captures log events and forwards them through an `mpsc` channel to a Non-Send resource.\n  - A system transfers messages from the channel into an `Event\u003cLogEvent\u003e`.\n  - You can then read `Event\u003cLogEvent\u003e` yourself, or use the provided system that prints via `repl_println!` so lines render above the prompt.\n\n- __API__\n  - Module: `bevy_repl::log_ecs`\n  - Layer hook for Bevy's `LogPlugin`: `repl_log_custom_layer`\n  - Event type: `LogEvent`\n  - Optional print system: `print_log_events_system`\n\n- __Recommended setup (preserve colors/format \u0026 avoid duplicate stdout)__\n\nIf you primarily want logs to print above the prompt with the usual colors/formatting, install the REPL-aware fmt layer and disable the native stdout logger. Importantly, call the installer BEFORE adding `DefaultPlugins`.\n\n```rust\nuse bevy::prelude::*;\nuse bevy_repl::prelude::*;\n\nfn main() {\n    // 1) Install REPL-aware fmt layer before plugins\n    tracing_to_repl_fmt();\n\n    App::new()\n        .add_plugins((\n            // 2) Disable Bevy's stdout logger to prevent duplicate/garbled output\n            DefaultPlugins.build().disable::\u003cbevy::log::LogPlugin\u003e(),\n            ReplPlugins.set(PromptPlugin::pretty()),\n        ))\n        .run();\n}\n```\n\n### Startup ordering (PostStartup)\n\n- __Why__: In pretty mode, the prompt reserves the bottom lines with a terminal scroll region. Startup prints (like instructions) should run after this region is established to avoid overlapping the prompt.\n- __How__: Use the global `ScrollRegionReadySet` to order your startup prints. This label exists in all builds; in minimal mode it’s a no-op.\n\n```rust\nuse bevy::prelude::*;\nuse bevy_repl::prelude::*;\n\nfn instructions() {\n    bevy_repl::repl_println!(\"Welcome!\");\n}\n\nfn main() {\n    App::new()\n        .add_plugins(ReplPlugins)\n        .add_systems(PostStartup, instructions.after(ScrollRegionReadySet))\n        .run();\n}\n```\n\n## Usage\n\n\u003e Note: When routing logs to the REPL (to keep formatting/colors and avoid prompt corruption), we recommend disabling Bevy's native stdout logger: `DefaultPlugins.build().disable::\u003cbevy::log::LogPlugin\u003e()`. Use the provided REPL-aware formatter (see Routing Bevy logs to the REPL) or a custom layer instead.\n\nThe REPL is designed to be used in headless mode, but it can be used in windowed\nmode too through the terminal while the app is running.\n\n### REPL lifecycle (v1)\n\nFor v1 there is no runtime toggle. The REPL is enabled when you add the plugin group and remains active for the run.\n\nTrigger commands by typing them in the REPL input buffer and pressing `Enter`.\nThe REPL will parse the command and trigger an event with the command's arguments\nand options.\n\n### Builder pattern (default)\n\n1. Make a Bevy event struct that represents the command and its arguments and\n   options. This is the event that will be triggered when the command is executed.\n2. Implement the `ReplCommand` trait for the event struct.\n   1. `fn clap_command() -\u003e clap::Command` - Use the `clap` builder pattern to\n      describe the command and its arguments or options.\n   2. `fn to_event(matches: \u0026clap::ArgMatches) -\u003e ReplResult\u003cSelf\u003e` - Implement\n      the `to_event` method to convert the command's arguments and options into\n      the event struct. This is where you validate the command's arguments\n      and options and map them to the event fields or return an error if they are\n      invalid. If the command has no arguments or options, return `Ok(Self)`.\n      **Tip:** If the command has no arguments or options, implement the `Default`\n      trait. You don't implement `to_event` in this case, since the default\n      implementation will return `Ok(Self)`.\n3. Add the command to the app with `.add_repl_command\u003cYourReplCommand\u003e()`.\n4. Add an observer for the command with `.add_observer(your_observer)`. The\n   observer is a one-shot system that receives a trigger event with the command's\n   arguments and options.\n\n```rust\nuse bevy::prelude::*;\nuse bevy_repl::prelude::*;\n\n#[derive(Debug, Clone, Event, Default)]\nstruct SimpleCommandWithoutArgs;\n\nimpl ReplCommand for SimpleCommandWithoutArgs {\n    fn clap_command() -\u003e clap::Command {\n        clap::Command::new(\"simple\")\n            .about(\"A simple command\")\n    }\n}\n\nfn on_simple(_trigger: Trigger\u003cSimpleCommandWithoutArgs\u003e) {\n    println!(\"You triggered a simple command without args\");\n}\n\nstruct CommandWithArgs {\n    arg1: String,\n    arg2: String,\n}\n\nimpl ReplCommand for CommandWithArgs {\n\n    fn clap_command() -\u003e clap::Command {\n        clap::Command::new(\"command\")\n            .about(\"A command with args\")\n            .arg(clap::Arg::new(\"arg1\").required(true))\n            .arg(clap::Arg::new(\"arg2\").required(true))\n    }\n\n    fn to_event(matches: \u0026clap::ArgMatches) -\u003e ReplResult\u003cSelf\u003e {\n        Ok(CommandWithArgs {\n            arg1: matches.get_one::\u003cString\u003e(\"arg1\").unwrap().clone(),\n            arg2: matches.get_one::\u003cString\u003e(\"arg2\").unwrap().clone(),\n        })\n    }\n}\n\nfn on_command_with_args(trigger: Trigger\u003cCommandWithArgs\u003e) {\n    println!(\"You triggered a command with args: {} {}\", trigger.arg1, trigger.arg2);\n}\n\nfn main() {\n    App::new()\n        .add_plugins((\n            // Run headless in the terminal\n            MinimalPlugins.set(\n                bevy::app::ScheduleRunnerPlugin::run_loop(\n                    Duration::from_secs_f32(1. / 60.)\n                )\n            ),\n            // Bevy input plugin is required to detect keyboard inputs\n            bevy::input::InputPlugin::default(),\n            // Default REPL stack (alternate screen, built-ins) with minimal renderer\n            ReplPlugins,\n        ))\n        .add_repl_command::\u003cSimpleCommandWithoutArgs\u003e()\n        .add_observer(on_simple)\n        .add_repl_command::\u003cCommandWithArgs\u003e()\n        .add_observer(on_command_with_args)\n        .run();\n}\n```\n\n### Derive pattern (requires `derive` feature)\n\nEnable the `derive` feature in your `Cargo.toml` to use the derive pattern.\n\n```toml\n[dependencies]\nbevy_repl = { version = \"0.3.0\", features = [\"derive\"] }\n```\n\nThen derive the `ReplCommand` trait on your command struct along with clap's\n`Parser` trait. Add the command to the app with `.add_repl_command\u003cYourReplCommand\u003e()`\nand add an observer for the command with `.add_observer(your_observer)` as usual.\n\n```rust\nuse bevy::prelude::*;\nuse bevy_repl::prelude::*;\nuse clap::Parser;\n\n#[derive(ReplCommand, Parser, Default, Event)]\nstruct SimpleCommandWithoutArgs;\n\n#[derive(ReplCommand, Parser, Event, Default)]\n#[clap(about = \"A command with args\")]\nstruct CommandWithArgs {\n    #[clap(short, long)]\n    arg1: String,\n    #[clap(short, long)]\n    arg2: String,\n}\n```\n\n### Prompt styling\n\n- __Appearance__\n  - Without the `pretty` feature (default): minimal prompt. One-line bar fixed to the bottom, showing only the prompt symbol and input buffer. No border, colors, or hint.\n  - With the `pretty` feature: enhanced prompt. Optional border with title, colored styles for title/prompt/hints, and a right-aligned usage hint.\n\n- __Compilation__\n  - Minimal build (no `pretty`): styling code is not compiled. No extra terminal manipulation beyond positioning the single-line prompt.\n  - Pretty build (`--features pretty`): styling code is compiled in and used by the renderer.\n\n- __Configuration__\n  - The prompt is configured via `ReplPromptConfig`. In minimal builds, only the `symbol` is honored; styling options are ignored.\n  - In pretty builds, you can use presets or customize:\n\n    ```rust\n    // ReplPlugins uses the minimal renderer by default.\n    // To enable the pretty renderer (with the `pretty` feature enabled), either:\n    //   - Set the plugin group: ReplPlugins.set(PromptPlugin::pretty())\n    //   - Or override visuals at runtime:\n    app.insert_resource(bevy_repl::prompt::ReplPromptConfig::pretty());\n    // or\n    app.insert_resource(bevy_repl::prompt::ReplPromptConfig::minimal());\n    // or explicit fields (pretty build):\n    app.insert_resource(bevy_repl::prompt::ReplPromptConfig {\n        symbol: Some(\"\u003e \".to_string()),\n        border: Some(bevy_repl::prompt::PromptBorderConfig::default()),\n        color: Some(bevy_repl::prompt::PromptColorConfig::default()),\n        hint: Some(bevy_repl::prompt::PromptHintConfig::default()),\n    });\n    ```\n\n  - To run the pretty example:\n    ```bash\n    cargo run --example pretty --features pretty\n    ```\n\n### Default keybinds\n\nWhen the REPL is enabled, the following keybinds are available:\n\n| Key | Action |\n| --- | --- |\n| `Enter` | Submit command |\n| `Esc` | Clear input buffer |\n| `Left/Right` | Move cursor |\n| `Home/End` | Jump to start/end of line |\n| `Backspace` | Delete character before cursor |\n| `Delete` | Delete character at cursor |\n| `Esc` | Clear input buffer |\n\nNote: `Ctrl+C` behaves like a normal terminal interrupt and is not handled by the REPL.\n\n## Design\n\n### Headless mode\n\n[\"Headless\" mode] is when a Bevy app runs in the terminal without a renderer. To\nrun Bevy in headless mode, disable all windowing features for Bevy in\n`Cargo.toml`. Then configure the schedule runner to loop forever instead of\nexiting the app after one frame. Running the app from the terminal only displays\nlog messages from the engine to the terminal and cannot accept input.\n\nNormally the open window keeps the app running, and the exit event happens when\nclosing the window. In headless mode there isn't a window to close, so the app\nruns until we kill the process or another system triggers the `AppExit` event\nwith a keycode event reader (like press Q to quit).\n\n[\"Headless\" mode]:\n    https://github.com/bevyengine/bevy/blob/main/examples/app/headless.rs\n\n```toml\n[dependencies]\nbevy = { version = \"*\", default-features = false }\n# replace \"*\" with the most recent version of bevy\n```\n\n```rust\nfn main() {\n    let mut app = App::new();\n\n    // Run in headless mode at 60 fps\n    app.add_plugins((\n        MinimalPlugins,\n        bevy::app::ScheduleRunnerPlugin::run_loop(\n            std::time::Duration::from_secs_f64(1.0 / 60.0),\n        )\n    ));\n\n    // Exit with Ctrl+C\n    app.run();\n}\n```\n\n### REPL Console\n\n`bevy_repl` takes the idea of a Half-Life 2 debug console and brings it to\nheadless mode, so an app can retain command style interaction without depending\non windowing, rendering, or UI features.\n\nInstead of rendering a fullscreen text user interface (TUI), which would kinda\ndefeat the purpose of headless mode, we render a small \"partial-TUI\" at the\nbottom of the terminal that supports keyboard input. The normal headless output\nis shifted up to make room for the input console, and everything else is\nprinted to the terminal normally. The app is truly running headless, and the\n\"partial-TUI\" is directly modifying the terminal output with `crossterm`.\n\nFancy REPL styling like a border and colors are available with the `pretty` feature.\n\n```toml\n[dependencies]\nbevy_repl = { version = \"0.3.0\", features = [\"default-commands\"] }\n```\n\n**REPL disabled (regular headless mode):**\n\n```text\n┌───your terminal──────────────────────────────────────────────────────────────┐\n│ INFO: 2025-07-28T12:00:00.000Z: bevy_repl: Starting REPL                     │\n│ INFO: 2025-07-28T12:00:00.000Z: bevy_repl: Type 'help' for commands          │\n│                                                                              │\n│ [Game logs and command output appear here...]                                │\n└──────────────────────────────────────────────────────────────────────────────┘\n```\n\n**REPL enabled:**\n\n```text\n┌───your terminal──────────────────────────────────────────────────────────────┐\n│ INFO: 2025-07-28T12:00:00.000Z: bevy_repl: Starting REPL                     │\n│ INFO: 2025-07-28T12:00:00.000Z: bevy_repl: Type 'help' for commands          │\n│                                                                              │\n│ [Game logs and command output appear here...]                                │\n│                                                                              │\n┌───REPL───────────────────────────────────────────────────────────────────────┐\n│ \u003e spawn-player Bob                                                           │\n└──────────────────────────────────────────────────────────────────────────────┘\n```\n\n### Command parsing\n\nInput is parsed via `clap` commands and corresponding observer systems that\nexecute when triggered by the command.\n\nUse clap's [builder pattern] to describe the command and its arguments or\noptions. Then add the command to the app with\n`.add_repl_command\u003cYourReplCommand\u003e()`. The REPL fires an event (e.g.\n`YourReplCommand`) when the command is parsed from the prompt.\n\nMake an observer for the command with `.add_observer(your_observer)`. The\nobserver is a one-shot system that receives a trigger event with the command's\narguments and options. As a system, it is executed in the `PostUpdate` schedule\nand has full access to the Bevy ECS.\n\n[builder pattern]: https://docs.rs/clap/latest/clap/_tutorial/index.html#tutorial-for-the-builder-api\n\n```rust\nuse bevy::prelude::*;\nuse bevy_repl::prelude::*;\n\nfn main() {\n    let frame_time = Duration::from_secs_f32(1. / 60.);\n\n    let mut app = App::new()\n        .add_plugins((\n            MinimalPlugins.set(ScheduleRunnerPlugin::run_loop(frame_time)),\n        ));\n\n    app.add_plugins((\n        ReplPlugin,\n        ReplDefaultCommandsPlugin,\n    ))\n    .add_repl_command::\u003cSayCommand\u003e()\n    .add_observer(on_say);\n\n    app.run();\n}\n\nstruct SayCommand {\n    message: String,\n}\n\nimpl ReplCommand for SayCommand {\n    fn command() -\u003e clap::Command {\n        clap::Command::new(\"say\")\n            .about(\"Say something\")\n            .arg(\n                clap::Arg::new(\"message\")\n                    .short('m')\n                    .long(\"message\")\n                    .help(\"Message to say\")\n                    .required(true)\n                    .takes_value(true)\n            )\n    }\n\n    fn to_event(matches: \u0026clap::ArgMatches) -\u003e ReplResult\u003cSelf\u003e {\n        Ok(SayCommand {\n            message: matches.get_all::\u003cString\u003e(\"message\").unwrap().join(\" \"),\n        })\n    }\n}\n\nfn on_say(trigger: Trigger\u003cSayCommand\u003e) {\n    println!(\"{}\", trigger.message);\n}\n```\n\n### Scheduling\n\nThe REPL reads input events and emits trigger events alongside the `bevy_ratatui`\n[input handling system set](https://github.com/cxreiff/bevy_ratatui/blob/main/src/crossterm_context/event.rs).\nThe REPL text buffer is updated and emits command triggers during\n`InputSet::EmitBevy`. The prompt is updated during `InputSet::Post` to reflect\nthe current state of the input buffer.\n\nAll REPL input systems run in the `Update` schedule, but as they are\nevent-based, they may not run every frame. Commands are executed in the\n`PostUpdate` schedule as observers.\n\nFor headless command output, use the regular `info!` or `debug!` macros and the\n`RUST_LOG` environment variable to configure messages printed to the console or\nimplement your own TUI panels with `bevy_ratatui`.\n\n### Directly modifying the terminal (to-do)\n\nThe REPL uses `crossterm` events generated by `bevy_ratatui` to read input events\nfrom the keyboard. When the REPL is enabled, the terminal is in raw mode and the\nREPL has direct access to the terminal cursor. The crate uses observers to\ndisable raw mode when the REPL is disabled or the app exits. If raw mode isn't\nhandled correctly, the terminal cursor may be left in an unexpected state.\n\nKeycode forwarding from crossterm to Bevy is disabled (except for the\nREPL toggle key) to avoid passing events to Bevy when you are typing a command.\nDisabling the REPL returns the terminal to normal headless mode, and keycodes\nare propagated to Bevy as normal.\n\nWe use Bevy keycode events for toggle behavior so that the REPL can be toggled\nwhen the terminal is NOT in raw mode. This is to avoid the need to place the\nterminal in raw mode even when the REPL is disabled. This is a tradeoff\nbetween simplicity and utility. It would be simpler to enable raw mode all the\ntime and detect raw keycode commands for the toggle key, then forward the raw\ninputs to Bevy as normal keycode events. However, this means that the app input\nhandling fundamentally changes, even when the REPL is disabled. For development,\nit is more useful to have the app behave exactly as a normal headless app when\nthe REPL is disabled to preserve consistency in input handling behavior.\n\n### Prompt styling\n\nThe REPL prompt supports two visual modes controlled by a simple resource and optional feature flag:\n\n- __Minimal__ (default baseline): 1-line bottom bar, no border/colors/hint.\n  - Opt-in at runtime with `PromptMinimalPlugin`:\n\n    ```rust\n    app.add_plugins(PromptMinimalPlugin);\n    ```\n\n- __Pretty__ (feature-gated): border, colorful title/prompt, right-aligned hint.\n  - Enable feature and run:\n\n    ```bash\n    cargo run --example pretty --features pretty\n    ```\n\n  - When the `pretty` feature is enabled, `ReplPlugins` uses the pretty preset automatically. You can still override visuals by inserting `ReplPromptConfig` at runtime.\n\nAdvanced users can customize visuals via the `ReplPromptConfig` resource:\n\n```rust\n// Use presets\napp.insert_resource(ReplPromptConfig::pretty());\n// or\napp.insert_resource(ReplPromptConfig::minimal());\n\n// Or customize explicitly\napp.insert_resource(ReplPromptConfig { border: true, color: false, hint: true });\n```\n## Known issues \u0026 limitations\n\n### Built-in `help` and `clear` commands are not yet implemented\nI have `help` and `clear` implemented as placeholders. I don't consider this\ncrate to be feature-complete until these are implemented.\n\n### Runtime toggle is not supported\nFor a true \"console\" experience, the REPL should be able to be toggled on and\noff at runtime. Ideally, you could run your headless application with it\ndisabled and then toggle it on when you need to debug.\n\nThis is not supported yet (believe me, I tried!) mostly because I was running\ninto too many issues with raw mode, crossterm events, and bevy events all at the\nsame time. It's definitely possible, but I haven't had the time to implement it.\n\n### Key events are not forwarded to Bevy\nAll key events are cleared by the REPL when it is enabled, so they are not\nforwarded to Bevy and causing unexpected behavior when typing in the prompt.\nThis is a tradeoff between simplicity and utility. It would be simpler to enable\nraw mode and detect raw keycode commands for the toggle key, then forward the\nraw inputs to Bevy as normal keycode events. However, this means that the app\ninput handling fundamentally changes, even when the REPL is disabled. For\ndevelopment, it is more useful to have the app behave exactly as a normal\nheadless app when the REPL is disabled to preserve consistency in input handling\nbehavior.\n\nIf you really need key events or button input while the REPL is enabled, you can place your event\nreader system _before_ the `ReplPlugin` in the app schedule. This will ensure\nthat your system is called before the REPL plugin, so keyboard and button\ninputs can be read before the REPL clears them.\n\n```rust\nApp::new()\n    .add_plugins((\n        MinimalPlugins.set(ScheduleRunnerPlugin::run_loop(Duration::from_secs_f64(1.0/60.0))),\n        // Minimal REPL: core + prompt + parser; main-screen rendering\n        MinimalReplPlugins,\n    ))\n    .add_systems(Update, your_event_reader_system.before(bevy_repl::ReplSet::Pre))\n    .run();\n```\n\n### Minimal renderer prompt does not scroll with terminal output\nThis is a limitation of the minimal renderer. The prompt is rendered in the\nterminal below the normal stdout, but it does not stay at the bottom of the\nterminal if there are other messages sent to stdout. The REPL works as expected\n(inputs are loaded to the buffer and commands are parsed and executed normally),\nbut the prompt may be hidden by other output.\n\nInstead of fixing this, I am focusing on the pretty prompt renderer, which\nresolves these issues at the cost of complexity and overhead. The pretty renderer\nuses a full TUI stack to render the prompt, which means it can stay at the bottom\nof the terminal and be visible even when other messages are sent to stdout. This\nalso means that it is an \"alternate screen\" from the main terminal, so it only\nshows text that is sent to the alternate screen.\n\nIf you don't want the pretty renderer, try to minimize outputs sent to stdout\nthat come from systems other than REPL command observers. This is pretty easy to\ndo by disabling `bevy::input::InputPlugin` or setting the max level log messages\nto be `warn` or `error`.\n\n### Pretty renderer log history doesn't scroll at all\nYou can't scroll up to see earlier logs in the history because the TUI doesn't\nhave scrolling enabled (yet). This is possible, just not implemented yet.\n\nIf you have a lot of logs or history is important, stick to the minimal\nrenderer.\n\n### Shift+\u003cChar\u003e aren't entered into the buffer\n`Shift + lowercase letter` is ignored by the prompt. This is because the prompt\ncaptures only characters, not chords. Since shift is a modifier, extra logic is\nneeded to support it. This is not implemented yet.\n\n## Aspirations\n- [x] **Derive pattern** - Describe commands with clap's derive pattern.\n- [ ] **Toggleable** - The REPL is disabled by default and can be toggled. When\n  disabled, the app runs normally in the terminal, no REPL systems run, and the\n  prompt is hidden.\n- [x] **Pretty prompt** - Show the prompt in the terminal below the normal\n  stdout, including the current buffer content.\n- [ ] **Scrolling pretty prompt** - The pretty renderer makes an alternate\n  screen but doesn't allow you to scroll up to see past input.\n- [x] **Support for games with rendering and windowing** - The REPL is designed to\n  work from the terminal, but the terminal normally prints logs when there is a\n  window too. The REPL still works from the terminal while using the window for\n  rendering if the console is enabled.\n- [ ] **Support for games with TUIs** - The REPL is designed to work as a sort of\n  sidecar to the normal terminal output, so _in theory_ it should be compatible\n  with games that use an alternate TUI screen. I don't know if it actually\n  works, probably only with the minimal renderer or perhaps a custom renderer.\n- [ ] **Customizable keybinds** - Allow the user to configure the REPL keybinds for\n  all REPL controls, not just the toggle key.\n- [ ] **Command history** - Use keybindings to navigate past commands\n- [ ] **Help text and command completion** - Use `clap`'s help text and completion\n  features to provide a better REPL experience and allow for command discovery.\n\n## License\n\nExcept where noted (below and/or in individual files), all code in this\nrepository is dual-licensed under either:\n\n- MIT License ([LICENSE-MIT](LICENSE-MIT) or\n  [http://opensource.org/licenses/MIT](http://opensource.org/licenses/MIT))\n- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or\n  [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0))\n\nat your option. This means you can select the license you prefer! This\ndual-licensing approach is the de-facto standard in the Rust ecosystem and there\nare [very good reasons](https://github.com/bevyengine/bevy/issues/2373) to\ninclude both.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fphiliplinden%2Fbevy_repl","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fphiliplinden%2Fbevy_repl","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fphiliplinden%2Fbevy_repl/lists"}