Keyboard shortcuts

Press โ† or โ†’ to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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));
}

Made with VHS

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 and tracing that shows Bevy logs with rich formatting in the REPL (if you disable Bevy's LogPlugin)
  • 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.

VersionBevyNotes
0.4.10.16.1Better docs: philiplinden.github.io/bevy_repl
0.4.00.16.1Removed 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.00.16.1First 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 FlagDescriptionDefault
deriveSupport clap's derive pattern for REPL commandsfalse
default_commandsEnable all built-in commandstrue
quitEnable the quit commandtrue (included in default_commands)
helpEnable the help commandfalse
clearEnable the clear commandfalse

Batteries-included setup

[dependencies]
bevy = "0.16.1"
bevy_repl = { version = "0.4.1", default-features = true }

Optional features:

Feature FlagDescriptionDefault
deriveSupport clap's derive pattern for REPL commandsfalse
default_commandsEnable all built-in commandstrue

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.

  1. Define a command type by deriving Event and implementing ReplCommand (or deriving it if you have the derive feature enabled).
  2. Register the command with the app with .add_repl_command::<YourReplCommand>().
  3. 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:

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 FlagDescriptionDefault
deriveSupport clap's derive pattern for REPL commandsfalse

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.

CommandAliasesDescriptionFeature FlagDefault
quitquit, q, exitGracefully terminate the applicationquittrue
helphelpShow available commandshelptrue
clearclearClear the screenclearfalse

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:

KeyAction
EnterSubmit command
EscClear input buffer
Left/RightMove cursor
Home/EndJump to start/end
BackspaceDelete before cursor
DeleteDelete at cursor
Ctrl+CTerminate 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)

#![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 an mpsc 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 via repl_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! over println! 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

Known rough edges and limitations (see README for latest details):

  • Built-in help and clear 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 the examples/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

๐Ÿงช 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

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

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

  8. 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.

  9. 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.