bevy_repl

An interactive REPL for headless Bevy apps powered by clap for command parsing
and bevy_ratatui for terminal input and output. The plugin adds a text input
area below the terminal output for interaction even in headless mode.
- Unobtrusive TUI console below normal terminal output
- Command parsing and CLI features from
clap - Observer-based command execution system with full Bevy ECS access for both read and write operations
- Logging integration with
bevy_logandtracingfor unified output display - Support for custom prompt rendering and minimal prompt mode
- Works in tandem with windowed apps from the terminal
- Built-in commands for common tasks (just
quitfor now)
The REPL is designed as an alternative to makspll/bevy-console for Bevy apps that want a terminal-like console to modify the game at runtime without implementing a full TUI or rendering features.
This is my first public Bevy plugin, and I vibe-coded a large part of it. You have been warned.
Table of Contents
Features
Theoretically all clap features are supported, but I have only tested derive.
Override the clap features in your Cargo.toml to enable or disable
additional features at your own risk.
Derive
Use the derive feature to support clap's derive pattern for REPL commands.
#[derive(ReplCommand)] will automatically implement the ReplCommand trait
and create an event with the command's arguments and options. Configure the
response by adding an observer for the REPL command like normal.
Built-in commands
Enable built-in commands with feature flags. Each command is enabled separately
by a feature flag. Use the default_commands feature to enable all built-in
commands.
| Feature Flag | Command | Description |
|---|---|---|
default_commands |
quit, help, clear |
Enable all built-in commands |
quit |
quit, q, exit |
Gracefully terminate the application |
help |
help |
Show clap help text (not yet implemented) |
clear |
clear |
Clear the terminal output |
Prompt styling
The prompt can be styled with the pretty feature. The feature adds a border,
colorful styles for title/prompt/hints, and a right-aligned hint text.
-
Minimal (default)
- Appearance: 1-line bottom prompt with symbol + input. No border/colors/hint.
- Compilation: no styling code compiled; lean terminal manipulation only.
- Config: only
ReplPromptConfig.symbolis honored. - Use:
cargo run(no extra feature flags).
-
Pretty (
--features pretty)- Appearance: border with title, colored styles, right-aligned usage hint.
- Compilation: styling code compiled and enabled.
- Config: presets or explicit
ReplPromptConfig { symbol, border, color, hint }. - Use
ReplPlugins.set(PromptPlugin::pretty())as shown below.
Custom renderer (feature-gated: pretty)
You can swap the prompt renderer at runtime by overriding the ActiveRenderer resource
with your own implementation of the PromptRenderer trait. This is the recommended
extension point for custom styles.
-
Build and run the demo custom renderer example:
-
Minimal usage (in your Bevy app):
use ; ; new .add_plugins .run;
The example and docs assume the pretty feature is enabled so the rendering
infrastructure is available. Custom renderers can ignore colors/borders entirely
if you want a minimal look.
Plugin groups and alternate screen
-
When is the alternate screen active?
- The alternate screen is active when
bevy_ratatui::RatatuiPluginsis added to your app. - Using
ReplPlugins(the default/turnkey group) automatically addsRatatuiPlugins, so the REPL renders in the alternate screen viaRatatuiContext. - Using
MinimalReplPluginsadds some but not all Ratatui Plugins; the prompt renders on the main terminal screen using the fallbackFallbackTerminalContext.
- The alternate screen is active when
-
Minimal (no alternate screen, no built-ins)
use ; use MinimalReplPlugins; use Duration; -
Default/turnkey (alternate screen + built-ins)
use ; use ReplPlugins; use Duration; -
How to choose
- Choose
MinimalReplPluginsif you:- Want to stay on the main terminal screen (no full TUI/pane UX).
- Intend to manage
bevy_ratatuior other input/render stacks yourself. - Prefer to opt-in to commands individually (no built-ins by default).
- Choose
ReplPluginsif you:- Want a turnkey setup with reliable prompt rendering in the alternate screen.
- Prefer sane defaults including built-in commands (
quit,help,clear). - Don’t need to wire
RatatuiPluginsmanually.
By default
ReplPluginsuses the minimal prompt renderer. To enable the pretty renderer when theprettyfeature is on, useReplPlugins.set(PromptPlugin::pretty()). - Choose
Robust printing in raw/alternate screen terminals
When 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!.
-
What it does
- Minimal renderer: moves the cursor to column 0 before printing, writes CRLF (
\r\n), and flushes stdout. - 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.
- Minimal renderer: moves the cursor to column 0 before printing, writes CRLF (
-
When to use
- Any time you print from systems/observers while the REPL is active
- Especially in raw mode or when using the alternate screen (e.g., with
ReplPlugins)
-
Example
If 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!.
Routing Bevy logs to the REPL
You can route logs produced by Bevy's tracing pipeline to the REPL so they appear above the prompt and scroll correctly.
-
How it works
- A custom
tracingLayer captures log events and forwards them through anmpscchannel to a Non-Send resource. - A system transfers messages from the channel into an
Event<LogEvent>. - You can then read
Event<LogEvent>yourself, or use the provided system that prints viarepl_println!so lines render above the prompt.
- A custom
-
API
- Module:
bevy_repl::log_ecs - Layer hook for Bevy's
LogPlugin:repl_log_custom_layer - Event type:
LogEvent - Optional print system:
print_log_events_system
- Module:
-
Recommended setup (preserve colors/format & avoid duplicate stdout)
If 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.
use *;
use *;
Startup ordering (PostStartup)
- 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.
- How: Use the global
ScrollRegionReadySetto order your startup prints. This label exists in all builds; in minimal mode it’s a no-op.
use *;
use *;
Usage
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::<bevy::log::LogPlugin>(). Use the provided REPL-aware formatter (see Routing Bevy logs to the REPL) or a custom layer instead.
The REPL is designed to be used in headless mode, but it can be used in windowed mode too through the terminal while the app is running.
REPL lifecycle (v1)
For v1 there is no runtime toggle. The REPL is enabled when you add the plugin group and remains active for the run.
Trigger commands by typing them in the REPL input buffer and pressing Enter.
The REPL will parse the command and trigger an event with the command's arguments
and options.
Builder pattern (default)
- Make a Bevy event struct that represents the command and its arguments and options. This is the event that will be triggered when the command is executed.
- Implement the
ReplCommandtrait for the event struct.fn clap_command() -> clap::Command- Use theclapbuilder pattern to describe the command and its arguments or options.fn to_event(matches: &clap::ArgMatches) -> ReplResult<Self>- Implement theto_eventmethod to convert the command's arguments and options into the event struct. This is where you validate the command's arguments and options and map them to the event fields or return an error if they are invalid. If the command has no arguments or options, returnOk(Self). Tip: If the command has no arguments or options, implement theDefaulttrait. You don't implementto_eventin this case, since the default implementation will returnOk(Self).
- Add the command to the app with
.add_repl_command<YourReplCommand>(). - Add an observer for the command with
.add_observer(your_observer). The observer is a one-shot system that receives a trigger event with the command's arguments and options.
use *;
use *;
;
Derive pattern (requires derive feature)
Enable the derive feature in your Cargo.toml to use the derive pattern.
[]
= { = "0.3.0", = ["derive"] }
Then derive the ReplCommand trait on your command struct along with clap's
Parser trait. Add the command to the app with .add_repl_command<YourReplCommand>()
and add an observer for the command with .add_observer(your_observer) as usual.
use *;
use *;
use Parser;
;
Prompt styling
-
Appearance
- Without the
prettyfeature (default): minimal prompt. One-line bar fixed to the bottom, showing only the prompt symbol and input buffer. No border, colors, or hint. - With the
prettyfeature: enhanced prompt. Optional border with title, colored styles for title/prompt/hints, and a right-aligned usage hint.
- Without the
-
Compilation
- Minimal build (no
pretty): styling code is not compiled. No extra terminal manipulation beyond positioning the single-line prompt. - Pretty build (
--features pretty): styling code is compiled in and used by the renderer.
- Minimal build (no
-
Configuration
-
The prompt is configured via
ReplPromptConfig. In minimal builds, only thesymbolis honored; styling options are ignored. -
In pretty builds, you can use presets or customize:
// ReplPlugins uses the minimal renderer by default. // To enable the pretty renderer (with the `pretty` feature enabled), either: // - Set the plugin group: ReplPlugins.set(PromptPlugin::pretty()) // - Or override visuals at runtime: app.insert_resource; // or app.insert_resource; // or explicit fields (pretty build): app.insert_resource; -
To run the pretty example:
-
Default keybinds
When the REPL is enabled, the following keybinds are available:
| Key | Action |
|---|---|
Enter |
Submit command |
Esc |
Clear input buffer |
Left/Right |
Move cursor |
Home/End |
Jump to start/end of line |
Backspace |
Delete character before cursor |
Delete |
Delete character at cursor |
Esc |
Clear input buffer |
Note: Ctrl+C behaves like a normal terminal interrupt and is not handled by the REPL.
Design
Headless mode
"Headless" mode is when a Bevy app runs in the terminal without a renderer. To
run Bevy in headless mode, disable all windowing features for Bevy in
Cargo.toml. Then configure the schedule runner to loop forever instead of
exiting the app after one frame. Running the app from the terminal only displays
log messages from the engine to the terminal and cannot accept input.
Normally the open window keeps the app running, and the exit event happens when
closing the window. In headless mode there isn't a window to close, so the app
runs until we kill the process or another system triggers the AppExit event
with a keycode event reader (like press Q to quit).
[]
= { = "*", = false }
# replace "*" with the most recent version of bevy
REPL Console
bevy_repl takes the idea of a Half-Life 2 debug console and brings it to
headless mode, so an app can retain command style interaction without depending
on windowing, rendering, or UI features.
Instead of rendering a fullscreen text user interface (TUI), which would kinda
defeat the purpose of headless mode, we render a small "partial-TUI" at the
bottom of the terminal that supports keyboard input. The normal headless output
is shifted up to make room for the input console, and everything else is
printed to the terminal normally. The app is truly running headless, and the
"partial-TUI" is directly modifying the terminal output with crossterm.
Fancy REPL styling like a border and colors are available with the pretty feature.
[]
= { = "0.3.0", = ["default-commands"] }
REPL disabled (regular headless mode):
┌───your terminal──────────────────────────────────────────────────────────────┐
│ INFO: 2025-07-28T12:00:00.000Z: bevy_repl: Starting REPL │
│ INFO: 2025-07-28T12:00:00.000Z: bevy_repl: Type 'help' for commands │
│ │
│ [Game logs and command output appear here...] │
└──────────────────────────────────────────────────────────────────────────────┘
REPL enabled:
┌───your terminal──────────────────────────────────────────────────────────────┐
│ INFO: 2025-07-28T12:00:00.000Z: bevy_repl: Starting REPL │
│ INFO: 2025-07-28T12:00:00.000Z: bevy_repl: Type 'help' for commands │
│ │
│ [Game logs and command output appear here...] │
│ │
┌───REPL───────────────────────────────────────────────────────────────────────┐
│ > spawn-player Bob │
└──────────────────────────────────────────────────────────────────────────────┘
Command parsing
Input is parsed via clap commands and corresponding observer systems that
execute when triggered by the command.
Use clap's builder pattern to describe the command and its arguments or
options. Then add the command to the app with
.add_repl_command<YourReplCommand>(). The REPL fires an event (e.g.
YourReplCommand) when the command is parsed from the prompt.
Make an observer for the command with .add_observer(your_observer). The
observer is a one-shot system that receives a trigger event with the command's
arguments and options. As a system, it is executed in the PostUpdate schedule
and has full access to the Bevy ECS.
use *;
use *;
Scheduling
The REPL reads input events and emits trigger events alongside the bevy_ratatui
input handling system set.
The REPL text buffer is updated and emits command triggers during
InputSet::EmitBevy. The prompt is updated during InputSet::Post to reflect
the current state of the input buffer.
All REPL input systems run in the Update schedule, but as they are
event-based, they may not run every frame. Commands are executed in the
PostUpdate schedule as observers.
For headless command output, use the regular info! or debug! macros and the
RUST_LOG environment variable to configure messages printed to the console or
implement your own TUI panels with bevy_ratatui.
Directly modifying the terminal (to-do)
The REPL uses crossterm events generated by bevy_ratatui to read input events
from the keyboard. When the REPL is enabled, the terminal is in raw mode and the
REPL has direct access to the terminal cursor. The crate uses observers to
disable raw mode when the REPL is disabled or the app exits. If raw mode isn't
handled correctly, the terminal cursor may be left in an unexpected state.
Keycode forwarding from crossterm to Bevy is disabled (except for the REPL toggle key) to avoid passing events to Bevy when you are typing a command. Disabling the REPL returns the terminal to normal headless mode, and keycodes are propagated to Bevy as normal.
We use Bevy keycode events for toggle behavior so that the REPL can be toggled when the terminal is NOT in raw mode. This is to avoid the need to place the terminal in raw mode even when the REPL is disabled. This is a tradeoff between simplicity and utility. It would be simpler to enable raw mode all the time and detect raw keycode commands for the toggle key, then forward the raw inputs to Bevy as normal keycode events. However, this means that the app input handling fundamentally changes, even when the REPL is disabled. For development, it is more useful to have the app behave exactly as a normal headless app when the REPL is disabled to preserve consistency in input handling behavior.
Prompt styling
The REPL prompt supports two visual modes controlled by a simple resource and optional feature flag:
-
Minimal (default baseline): 1-line bottom bar, no border/colors/hint.
-
Opt-in at runtime with
PromptMinimalPlugin:app.add_plugins;
-
-
Pretty (feature-gated): border, colorful title/prompt, right-aligned hint.
-
Enable feature and run:
-
When the
prettyfeature is enabled,ReplPluginsuses the pretty preset automatically. You can still override visuals by insertingReplPromptConfigat runtime.
-
Advanced users can customize visuals via the ReplPromptConfig resource:
// Use presets
app.insert_resource;
// or
app.insert_resource;
// Or customize explicitly
app.insert_resource;
Known issues & limitations
Built-in help and clear commands are not yet implemented
I have help and clear implemented as placeholders. I don't consider this
crate to be feature-complete until these are implemented.
Runtime toggle is not supported
For a true "console" experience, the REPL should be able to be toggled on and off at runtime. Ideally, you could run your headless application with it disabled and then toggle it on when you need to debug.
This is not supported yet (believe me, I tried!) mostly because I was running into too many issues with raw mode, crossterm events, and bevy events all at the same time. It's definitely possible, but I haven't had the time to implement it.
Key events are not forwarded to Bevy
All key events are cleared by the REPL when it is enabled, so they are not forwarded to Bevy and causing unexpected behavior when typing in the prompt. This is a tradeoff between simplicity and utility. It would be simpler to enable raw mode and detect raw keycode commands for the toggle key, then forward the raw inputs to Bevy as normal keycode events. However, this means that the app input handling fundamentally changes, even when the REPL is disabled. For development, it is more useful to have the app behave exactly as a normal headless app when the REPL is disabled to preserve consistency in input handling behavior.
If you really need key events or button input while the REPL is enabled, you can place your event
reader system before the ReplPlugin in the app schedule. This will ensure
that your system is called before the REPL plugin, so keyboard and button
inputs can be read before the REPL clears them.
new
.add_plugins
.add_systems
.run;
Minimal renderer prompt does not scroll with terminal output
This is a limitation of the minimal renderer. The prompt is rendered in the terminal below the normal stdout, but it does not stay at the bottom of the terminal if there are other messages sent to stdout. The REPL works as expected (inputs are loaded to the buffer and commands are parsed and executed normally), but the prompt may be hidden by other output.
Instead of fixing this, I am focusing on the pretty prompt renderer, which resolves these issues at the cost of complexity and overhead. The pretty renderer uses a full TUI stack to render the prompt, which means it can stay at the bottom of the terminal and be visible even when other messages are sent to stdout. This also means that it is an "alternate screen" from the main terminal, so it only shows text that is sent to the alternate screen.
If you don't want the pretty renderer, try to minimize outputs sent to stdout
that come from systems other than REPL command observers. This is pretty easy to
do by disabling bevy::input::InputPlugin or setting the max level log messages
to be warn or error.
Pretty renderer log history doesn't scroll at all
You can't scroll up to see earlier logs in the history because the TUI doesn't have scrolling enabled (yet). This is possible, just not implemented yet.
If you have a lot of logs or history is important, stick to the minimal renderer.
Shift+ aren't entered into the buffer
Shift + lowercase letter is ignored by the prompt. This is because the prompt
captures only characters, not chords. Since shift is a modifier, extra logic is
needed to support it. This is not implemented yet.
Aspirations
- Derive pattern - Describe commands with clap's derive pattern.
- Toggleable - The REPL is disabled by default and can be toggled. When disabled, the app runs normally in the terminal, no REPL systems run, and the prompt is hidden.
- Pretty prompt - Show the prompt in the terminal below the normal stdout, including the current buffer content.
- Scrolling pretty prompt - The pretty renderer makes an alternate screen but doesn't allow you to scroll up to see past input.
- Support for games with rendering and windowing - The REPL is designed to work from the terminal, but the terminal normally prints logs when there is a window too. The REPL still works from the terminal while using the window for rendering if the console is enabled.
- Support for games with TUIs - The REPL is designed to work as a sort of sidecar to the normal terminal output, so in theory it should be compatible with games that use an alternate TUI screen. I don't know if it actually works, probably only with the minimal renderer or perhaps a custom renderer.
- Customizable keybinds - Allow the user to configure the REPL keybinds for all REPL controls, not just the toggle key.
- Command history - Use keybindings to navigate past commands
- Help text and command completion - Use
clap's help text and completion features to provide a better REPL experience and allow for command discovery.
License
Except where noted (below and/or in individual files), all code in this repository is dual-licensed under either:
- MIT License (LICENSE-MIT or http://opensource.org/licenses/MIT)
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
at your option. This means you can select the license you prefer! This dual-licensing approach is the de-facto standard in the Rust ecosystem and there are very good reasons to include both.