bevy_repl
A Bevy plugin that provides a Read-Eval-Print Loop (REPL) interface for interactive command input.
The ReplPlugins
plugin group enables a REPL within the terminal while your
Bevy application runs, allowing users to enter commands and interact with the
ECS at runtime.
use bevy::prelude::*; use bevy_repl::prelude::*; fn main() { App::new().add_plugins((DefaultPlugins, ReplPlugins)); }
Bevy REPL is 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 to stdout
- Command parsing and CLI features from
clap
- Observer-based command execution system with full Bevy ECS access for both read and write operations
- Integration with
bevy_log
andtracing
that shows Bevy logs with rich formatting in the REPL (if you disable Bevy'sLogPlugin
) - Works in terminal with headless and windowed apps
- Built-in commands for common tasks (just
quit
for now) - Support for custom prompt rendering
- (Experimental) Custom keybind support for REPL cursor controls
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.
Version | Bevy | Notes |
---|---|---|
0.4.1 | 0.16.1 | Better docs: philiplinden.github.io/bevy_repl |
0.4.0 | 0.16.1 | Removed the "pretty" renderer in favor of getting simple prompt features working. Changed the interface slightly. This is a breaking change! See examples for help. |
0.3.0 | 0.16.1 | First release. Supports derive feature. Only quit built-in command is implemented. Includes a "pretty" renderer for fancy prompt styling, but it doesn't work very well. |
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.
Feature Flag | Description | Default |
---|---|---|
derive | Support clap's derive pattern for REPL commands | false |
default_commands | Enable all built-in commands | true |
quit | Enable the quit command | true (included in default_commands ) |
help | Enable the help command | false |
clear | Enable the clear command | false |
Batteries-included setup
[dependencies]
bevy = "0.16.1"
bevy_repl = { version = "0.4.1", default-features = true }
Optional features:
Feature Flag | Description | Default |
---|---|---|
derive | Support clap's derive pattern for REPL commands | false |
default_commands | Enable all built-in commands | true |
ReplPlugins
use bevy::prelude::*; use bevy_repl::ReplPlugins; fn main() { App::new() // Headless with a stable frame time (60 FPS) - this is important! .add_plugins(( DefaultPlugins .set(bevy::app::ScheduleRunnerPlugin::run_loop( std::time::Duration::from_secs_f32(1.0/60.0) )) ReplPlugins, )) .run(); }
ReplCommand
Input is parsed via clap
commands and corresponding observer systems that
execute when triggered by the command.
- Define a command type by deriving
Event
and implementingReplCommand
(or deriving it if you have thederive
feature enabled). - Register the command with the app with
.add_repl_command::<YourReplCommand>()
. - Handle the command event with an observer:
.add_observer(on_command)
.
The REPL parses prompt input to a YourReplCommand
event, where the fields are
the parsed arguments and options. Use observers to handle the event with full
ECS access.
Bevy REPL Book
The Bevy REPL Book is a collection of docs and notes about the Bevy REPL, how to use it, and how it works under the hood.
The book is available at philiplinden.github.io/bevy_repl.
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.
Features
| 0.4.0 | 0.16.1 | Removed the "pretty" renderer in favor of getting simple prompt features working. Changed the interface slightly. This is a breaking change! See examples for help. |
| 0.3.0 | 0.16.1 | First release. Supports derive
feature. Only quit
built-in command is implemented. Includes a "pretty" renderer for fancy prompt styling, but it doesn't work very well. |
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.
Feature Flag | Description | Default |
---|---|---|
derive | Support clap's derive pattern for REPL commands | false |
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.
Enable the derive
feature to use clap's derive pattern with #[derive(ReplCommand)]
.
[dependencies]
bevy_repl = { version = "0.4", features = ["derive"] }
use bevy::prelude::*; use bevy_repl::prelude::*; use clap::Parser; #[derive(ReplCommand, Parser, Default, Event)] struct Ping; fn on_ping(_t: Trigger<Ping>) { println!("pong"); } fn main() { App::new() .add_plugins(( MinimalPlugins, bevy::input::InputPlugin::default(), ReplPlugins, )) .add_repl_command::<Ping>() .add_observer(on_ping) .run(); }
Default 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.
Command | Aliases | Description | Feature Flag | Default |
---|---|---|---|---|
quit | quit , q , exit | Gracefully terminate the application | quit | true |
help | help | Show available commands | help | true |
clear | clear | Clear the screen | clear | false |
quit
Usage: quit
Aliases: q
, exit
quit
gracefully terminates the application by sending an AppExit
event in
ECS. This is the preferred way to exit a Bevy application. Unlike a simple
Ctrl+C
or SIGINT
, sending the AppExit
event ensures that all of the
application's resources are cleaned up properly, including the REPL.
Bevy REPL has an observer that restores the terminal state when the AppExit
event is read, so you can build your own quit command if you want. The important
thing is that the REPL modifies the terminal state (it puts the terminal in
"raw mode") and the cleanup ensures that "raw mode" is disabled when the app
exits. If raw mode is not disabled, the terminal may behave in unexpected ways
even after the app has exited.
help
Usage: help
Aliases: None
Shows all commands available to the REPL. (Not implemented)
clear
Usage: clear
Aliases: None
Clears the screen. (Not implemented)
Usage
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.
It is not possible to toggle the REPL on and off at runtime (yet!).
Add bevy_repl::ReplPlugins
to your app to enable the REPL and print logs to
stdout. By default, the REPL includes a quit
command to terminate the app.
Add a command to the app with .add_repl_command<YourReplCommand>()
. The
command struct must implement the Event
and ReplCommand
traits. When the
user enters a command, the REPL parses it with clap
and emits an event with the
command's arguments and options as the fields of the event.
Add an observer for the command with .add_observer(your_observer)
. The
observer is a one-shot system that receives the event and can perform any action
it needs to with full ECS access, and is a feature included in Bevy. For more
information about observers, see: Bevy examples.
Builder pattern
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 when the command
is parsed from the prompt. The REPL command struct is also the event. When it is
read by an observer or event reader, you can treat the command as an ordinary
event where its fields are the parsed arguments and options.
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 bevy::prelude::*; use bevy_repl::prelude::*; fn main() { let frame_time = Duration::from_secs_f32(1. / 60.); let mut app = App::new() .add_plugins(( MinimalPlugins.set(ScheduleRunnerPlugin::run_loop(frame_time)), )); app.add_plugins(( ReplPlugin, ReplDefaultCommandsPlugin, )) .add_repl_command::<SayCommand>() .add_observer(on_say); app.run(); } struct SayCommand { message: String, } impl ReplCommand for SayCommand { fn command() -> clap::Command { clap::Command::new("say") .about("Say something") .arg( clap::Arg::new("message") .short('m') .long("message") .help("Message to say") .required(true) .takes_value(true) ) } fn to_event(matches: &clap::ArgMatches) -> ReplResult<Self> { Ok(SayCommand { message: matches.get_all::<String>("message").unwrap().join(" "), }) } } fn on_say(trigger: Trigger<SayCommand>) { println!("{}", trigger.message); }
Derive pattern (requires derive
feature)
Enable the derive
feature in your Cargo.toml
to use the derive pattern.
Example: derive.rs
[dependencies]
bevy_repl = { version = "0.3.1", features = ["derive"] }
Then derive the ReplCommand
trait on your command struct along with clap's
Parser
trait. Add the command and its observer to the app as usual.
use bevy::prelude::*; use bevy_repl::prelude::*; use clap::Parser; #[derive(ReplCommand, Parser, Default, Event)] struct CommandWithoutArgs; fn on_command_without_args(_trigger: Trigger<CommandWithoutArgs>) { println!("You triggered a command without args"); } #[derive(ReplCommand, Parser, Event, Default)] #[clap(about = "A command with args")] struct CommandWithArgs { #[clap(short, long)] arg1: String, #[clap(short, long)] arg2: String, } fn on_command_with_args(trigger: Trigger<CommandWithArgs>) { println!("You triggered a command with args: {} {}", trigger.arg1, trigger.arg2); } fn main() { App::new() .add_plugins(( // Run headless in the terminal MinimalPlugins.set( bevy::app::ScheduleRunnerPlugin::run_loop( Duration::from_secs_f32(1. / 60.) ) ), // Bevy input plugin is required to detect keyboard inputs bevy::input::InputPlugin::default(), // Default REPL stack (ratatui, prompt, and built-in commands) ReplPlugins, )) .add_repl_command::<CommandWithoutArgs>() .add_observer(on_command_without_args) .add_repl_command::<CommandWithArgs>() .add_observer(on_command_with_args) .run(); }
Configuration
Feature flags, plugins, and runtime options.
- See crate features in
Cargo.toml
. - Configure plugins and renderers in your app's setup code.
- Check examples for common configurations.
Keybinds
This page explains the default keybinds and how to customize them via
PromptKeymap
.
See examples/keybinds.rs
for a runnable setup that configures PromptKeymap
.
cargo run --example keybinds
Default keybinds
The following keys control the REPL input buffer by default:
Key | Action |
---|---|
Enter | Submit command |
Esc | Clear input buffer |
Left/Right | Move cursor |
Home/End | Jump to start/end |
Backspace | Delete before cursor |
Delete | Delete at cursor |
Ctrl+C | Terminate app (signal) |
warning
Ctrl+C behaves like a normal terminal interrupt because Bevy REPL
installs a safety hook to handle SIGINT
(Ctrl+C) and restore the terminal
(disable raw mode) on exit. This works even if a quit command is disabled but
also does not allow to use Ctrl+C to be mapped to other actions.
Customizing keybinds
Keybinds are configured with the PromptKeymap
resource in bevy_repl::prompt::keymap
.
Each action maps to an exact (KeyCode, KeyModifiers)
pair as a ReplKeybind
.
important
The REPL uses Crossterm keycodes and modifiers to capture input, NOT Bevy keycodes and modifiers.
#![allow(unused)] fn main() { use bevy_ratatui::crossterm::event::{KeyCode as CrosstermKeyCode, KeyModifiers}; }
Examples of combinations
- v:
ReplKeybind { code: CrosstermKeyCode::Char('v'), mods: KeyModifiers::NONE }
- Shift+v:
ReplKeybind { code: CrosstermKeyCode::Char('V'), mods: KeyModifiers::SHIFT }
- Ctrl+v:
ReplKeybind { code: CrosstermKeyCode::Char('v'), mods: KeyModifiers::CONTROL }
- Ctrl+Shift+v:
ReplKeybind { code: CrosstermKeyCode::Char('V'), mods: KeyModifiers::CONTROL | KeyModifiers::SHIFT }
- Ctrl+Alt+Shift+v:
ReplKeybind { code: CrosstermKeyCode::Char('V'), mods: KeyModifiers::CONTROL | KeyModifiers::ALT | KeyModifiers::SHIFT }
Capital letters and Shift
Terminals often report Shifted letters as uppercase KeyCode::Char('V')
and may also set SHIFT
.
Match both code
and mods
exactly in your binding.
By default, the fallback โinsert printable charโ only fires for unmodified keys (no modifiers).
If you want Shift-only typing (e.g., Shift+v
-> V
) to insert without an explicit binding,
you can relax the fallback policy inside PromptKeymap::map
:
#![allow(unused)] fn main() { // inside PromptKeymap::map fallback use bevy_ratatui::crossterm::event::KeyModifiers as M; if self.allow_plain_char_insert { if let KeyCode::Char(c) = event.code { if event.modifiers.is_empty() || event.modifiers == M::SHIFT { return Some(ReplBufferEvent::Insert(c)); } } } }
Advanced mappings & Kitty protocol
Ratatui uses Kitty protocol by default, which is necessary for advanced keybinds
like Ctrl+Enter. For now, this is not supported in the REPL natively, but you
can use the REPL together with bevy_ratatui
and may have better results.
See examples/alt_screen.rs
for a runnable setup that uses bevy_ratatui
.
Prompt styling
The REPL uses bevy_ratatui
for rendering the prompt and input buffer. The
prompt renderer is configured via ReplPromptConfig
. The default renderer is a
simple 1-line bottom prompt with a symbol and input buffer.
For now, we only support a "partial-TUI" approach where the REPL and terminal
outputs are rendered to the main terminal screen. Ratatui alternate screens are
available if you add bevy_ratatui::RatatuiPlugins
to your app before
ReplPlugins
. Support for Ratatui alternate screens is experimental.
use bevy::prelude::*; fn main() { App::new() .add_plugins(( DefaultPlugins, bevy_ratatui::RatatuiPlugins::default(), bevy_repl::ReplPlugins, )) .run(); }
Example: alt_screen.rs
The REPL prompt supports basic configuration via the ReplPromptConfig
resource.
You can configure the prompt symbol:
#![allow(unused)] fn main() { app.insert_resource(ReplPromptConfig { symbol: Some("> ".to_string()) }); }
More advanced prompt styling is not yet implemented for the default prompt renderer. It is possible to do advanced TUI styling with a custom renderer, though. See examples/custom_renderer.rs.
Design
This section documents the design of the REPL and its components. I include it here as a reference for myself and for anyone who wants to understand how the REPL works.
Headless Bevy
The REPL is designed to be an interactive console for the Bevy app at runtime. It runs in the terminal while your Bevy app is running, even in headless mode.
"Headless" mode is when a Bevy app runs in the terminal without a window. All
systems run as normal, such as input detection and asset loading, but the app
exits after one loop iteration unless it is configured to run indefinitely. The
app runs headless if the bevy_window
feature is disabled or the WindowPlugin
is disabled.
Bevy headless examples:
- https://github.com/bevyengine/bevy/blob/main/examples/app/headless.rs
- https://github.com/bevyengine/bevy/blob/main/examples/app/headless_renderer.rs
Headless app with default features except windowing
The preferred way to run a Bevy app headless is to disable default bevy features
and explicitly add the desire features, leaving out bevy_winit
and
bevy_window
. (Note that Bevy Repl requires bevy_log
and trace
features.)
[dependencies]
bevy = {
version = "*", # replace "*" with the most recent version of bevy
default-features = false,
features = [
"bevy_log", "trace", # Bevy REPL needs `bevy_log` and `trace`.
# include all the other feature flags you need here.
# see: https://docs.rs/bevy/latest/bevy/#features
]
}
use bevy::prelude::*; fn main() { let mut app = App::new(); app.add_plugins(( // with bevy_window and bevy_winit disabled, those plugins aren't in // DefaultPlugins. All we have to do is tell the runner to run at 60 fps // so it doesn't consume the whole CPU core. DefaultPlugins, bevy::app::ScheduleRunnerPlugin::run_loop( std::time::Duration::from_secs_f64(1.0 / 60.0), ) )); // Exit with Ctrl+C app.run(); }
Minimal headless app (no default features)
Even with all the default features, Bevy ships MinimalPlugins
with the minimum
set of plugins required to run a Bevy app. Be sure to also enable InputPlugin
so the app can handle keyboard inputs, like for the REPL or Ctrl+C interrupts.
use bevy::prelude::*; fn main() { let mut app = App::new(); // Run in headless mode at 60 fps app.add_plugins(( MinimalPlugins, bevy::input::InputPlugin::default(), // The ScheduleRunnerPlugin handles the app run loop. In a headless Bevy // app (no window) using the schedule runner with no frame wait // configured, the loop runs as fast as possible (busy-loop on native), // consuming a core. Run at 60 fps so it doesn't melt your CPU. bevy::app::ScheduleRunnerPlugin::run_loop( std::time::Duration::from_secs_f64(1.0 / 60.0), ) )); // Exit with Ctrl+C app.run(); }
Headless app with default features and windowing disabled
If you need to keep the windowing features, you can disable the WindowPlugin
and WinitPlugin
to run the app in headless mode.
[!TIP] Bevy REPL still runs in the terminal even if you spawn windows, so this is probably only useful if you are running the app in CI or some other headless environment.
use bevy::{ prelude::*, // WindowPlugin is in the prelude window::ExitCondition, winit::WinitPlugin, }; fn main() { let mut app = App::new(); app.add_plugins(( DefaultPlugins .set(WindowPlugin { // Don't make a new window at startup primary_window: None, // Donโt automatically exit due to having no windows. // Instead, run until an `AppExit` event is produced. exit_condition: ExitCondition::DontExit, ..default() }) // WinitPlugin will panic in environments without a display server. .disable::<WinitPlugin>(), // We still want to set the FPS so the app doesn't melt the CPU. // ScheduleRunnerPlugin replaces the bevy_winit app runner, though, so // disabling the windowing plugins is redundant. bevy::app::ScheduleRunnerPlugin::run_loop( std::time::Duration::from_secs_f64(1.0 / 60.0), ), )); // Exit with Ctrl+C app.run(); }
Command Parsing
Input is parsed via clap
commands and corresponding observer systems that
execute when triggered by the REPL.
- Minimal example (builder pattern)
- Capturing crossterm key events
- Key events are not forwarded to Bevy while the REPL is enabled
Minimal example (builder pattern)
#![allow(unused)] fn main() { use bevy::prelude::*; use bevy_repl::prelude::*; #[derive(Debug, Clone, Event, Default)] struct Say { msg: String } impl ReplCommand for Say { fn clap_command() -> clap::Command { clap::Command::new("say").arg(clap::Arg::new("msg").required(true)) } fn to_event(m: &clap::ArgMatches) -> ReplResult<Self> { Ok(Say { msg: m.get_one::<String>("msg").unwrap().clone() }) } } fn on_say(t: Trigger<Say>) { println!("{}", t.msg); } }
See examples/
for more.
Capturing crossterm key events
The REPL captures crossterm key events and emits them as ReplBufferEvent
after
matching the key against the keymap. If no binding matches a REPL action (Clear,
Backspace, Delete, Left, Right, Home, End, Submit command) the key is stored in
the input buffer as a character.
Input parsing is logged at the trace level as seen in the show_prompt_actions example:
2025-08-29T02:39:41.436320Z TRACE: bevy_repl::prompt::input: Insert('h')
2025-08-29T02:39:41.606070Z TRACE: bevy_repl::prompt::input: Insert('e')
2025-08-29T02:39:42.890644Z TRACE: bevy_repl::prompt::input: Insert('l')
2025-08-29T02:39:43.059817Z TRACE: bevy_repl::prompt::input: Insert('l')
2025-08-29T02:39:43.363180Z TRACE: bevy_repl::prompt::input: Insert('o')
2025-08-29T02:45:18.595779Z TRACE: bevy_repl::prompt::input: Submit
2025-08-29T02:45:18.612872Z ERROR: bevy_repl::command::parser: Unknown command 'hello'
After the input parsing system, the REPL plugin clears key events and stops
keyboard input from being forwarded to Bevy when REPL is enabled. This prevents
key events from reaching game systems while typing into the prompt. The REPL
clears Crossterm key events and Bevy key events spawned by bevy_ratatui
.
Key events can be parsed before the REPL clears them by placing systems in or
before the ReplSet::Pre
set. This is useful for wiring up keys that manage the
REPL itself. See the
keybinds
example for a demonstration.
Key events are not forwarded to Bevy while the REPL is enabled
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 in or before ReplSet::Pre
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.
#![allow(unused)] fn main() { App::new() .add_plugins(( MinimalPlugins.set(ScheduleRunnerPlugin::run_loop(Duration::from_secs_f64(1.0/60.0))), ReplPlugins, )) .add_systems(Update, your_event_reader_system.in_set(bevy_repl::ReplSet::Pre)) .run(); }
Routing Bevy logs to the REPL
By default, Bevy REPL integrates with Bevy's LogPlugin
without additional
setup. To only print the REPL to stdout, disable Bevy's LogPlugin
.
Using an alternate TUI screen (experimental)
If you are using an alternate TUI screen (like with RatatuiPlugins
), Bevy log
messages will not be visible in the REPL unless you disable Bevy's LogPlugin
.
If the Ratatui context is enabled (e.g.,
bevy_ratatui::RatatuiPlugins::default()
or
bevy_ratatui::context::ContextPlugin
is added to the app), the REPL handles
log routing like so:
- A custom
tracing
Layer captures log events and forwards them through anmpsc
channel 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.
use bevy::prelude::*; use bevy_repl::prelude::*; fn main() { App::new() .add_plugins(( DefaultPlugins.build().disable::<bevy::log::LogPlugin>(), bevy_ratatui::RatatuiPlugins::default(), ReplPlugins, )) .run(); }
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
.
Terminal Screens
Ratatui TUIs often use an alternate screen (separate from stdout). Bevy REPL favors a "partial-TUI" that renders the prompt while keeping stdout usable.
- When REPL is active, the terminal runs in raw mode and prints to stdout.
- Prefer
bevy_repl::repl_println!
overprintln!
while REPL is active to avoid cursor/newline glitches. - If you enable a full alternate screen via
bevy_ratatui::RatatuiPlugins
, REPL still works but output behavior changes.
repl_println!
ensures safe, consistent output:
#![allow(unused)] fn main() { fn on_ping(_t: Trigger<Ping>) { bevy_repl::repl_println!("Pong"); } fn instructions() { bevy_repl::repl_println!(); bevy_repl::repl_println!("Welcome to the Bevy REPL!"); } }
Development
This chapter contains developer notes, to-dos, known issues, and other information for those who want to contribute to the crate.
Aspirations
- Derive pattern (Added in v0.3.0) - Describe commands with clap's derive pattern.
- Support for games with rendering and windowing (Added in v0.3.0) - 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.
- Printing to stdout (Added in v0.4.0) - The REPL should print to stdout instead of the TUI screen unless the user explicitly enables a TUI context that uses the alternate screen.
- Toggleable (Added in v0.4.1) - 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.
- Scrollable TUI output - The terminal output on the TUI screen should scroll to show past messages like a normal terminal screen printing to stdout.
- 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 (Added in v0.4.1) - 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 and insert them in the prompt buffer.
-
Help text and command completion - Use
clap
's help text and completion features to provide a better REPL experience and allow for command discovery. - Customizable prompt - Allow the user to configure the REPL prompt for all REPL controls, not just the toggle key.
Known Issues & Limitations
- Built-in
help
andclear
commands are not yet implemented - Terminal behavior is inconsistent between Windows and Linux
- Keybinds with modifier keys are not reliably detected
Known rough edges and limitations (see README for latest details):
- Built-in
help
andclear
commands are not yet implemented. - Ctrl+Enter and other advanced key combinations do not work.
- Directly modifying the terminal can be fragile if raw mode isn't restored.
Tips:
- Place your input event reader system before
bevy_repl::ReplSet::Pre
if you need to read inputs while REPL is enabled. - If the terminal state is left odd after an abnormal exit, restart your terminal.
- If you are on Windows, use the REPL with
bevy_ratatui
added too. (See theexamples/alt_screen.rs
example.)
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.
Terminal behavior is inconsistent between Windows and Linux
The input buffer and cursor behavior is inconsistent between Windows and Linux. On Linux, the cursor is always visible and input appears in the buffer as it is typed. On Windows, the cursor and input buffer are not visible while typing. The buffer is clearly interpreted as normal, but the user can't see it.
Interestingly, the cursor and input buffer are visible while typing in the
prompt when using the bevy_ratatui
crate in conjunction with bevy_repl
.
Keybinds with modifier keys are not reliably detected
This might be related to not using Kitty protocol.
Changelog
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.
See GitHub Releases and the Bevy REPL Book for details.
0.4.1 - 2025-08-29
๐ Documentation
- Add mdbook plugins (
9b8ebd4
) - Introducing the Bevy REPL Book (
b7710e0
) - Update readme toc (
e4390a3
) - Update readme (
43493fc
) - Add docs for other design experiments (
b1b2abc
)
๐งช Experimental
- Context refactor and better keybinds (
dd28099
) - Custom keybinds and cleaner examples (
2209b9d
) - Add placeholders for help (
4354571
) - Remove scrollreadyset, always stdout (
bed7d47
) - Move stdout behind feature flag (
a6460bc
) - Make context mgmt better (
8b0db88
) - Remove pretty stuff (
ce50975
)
โ๏ธ Repository
- (docs) Build mdBook on PR, deploy only on tags using mdBook action (
2e090cd
) - Use the real github action for pages (
06030f1
) - Deploy the book on every push to main (
c4589a2
) - Make the book on every push to main (
563574c
) - Enable changelog generation (
82832ca
)
License
This project is dual-licensed under Apache-2.0 or MIT, at your option.
Apache-2.0
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
Definitions.
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
-
Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
-
Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
-
Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
-
Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
-
Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
-
Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
-
Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
-
Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
MIT
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.