bevy_repl 0.3.0

Add a REPL to headless Bevy applications
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
# bevy_repl


![Made with VHS](https://vhs.charm.sh/vhs-6kUt4mnyvUcmbpVfWzHx4s.gif)

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_log` and `tracing` for 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 `quit` for now)

The REPL is designed as an alternative to
[makspll/bevy-console](https://github.com/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


- [bevy\_repl]#bevy_repl
  - [Table of Contents]#table-of-contents
  - [Features]#features
    - [Derive]#derive
    - [Built-in commands]#built-in-commands
    - [Prompt styling]#prompt-styling
      - [Custom renderer (feature-gated: `pretty`)]#custom-renderer-feature-gated-pretty
      - [Plugin groups and alternate screen]#plugin-groups-and-alternate-screen
    - [Robust printing in raw/alternate screen terminals]#robust-printing-in-rawalternate-screen-terminals
    - [Routing Bevy logs to the REPL]#routing-bevy-logs-to-the-repl
    - [Startup ordering (PostStartup)]#startup-ordering-poststartup
  - [Usage]#usage
    - [REPL lifecycle (v1)]#repl-lifecycle-v1
    - [Builder pattern (default)]#builder-pattern-default
    - [Derive pattern (requires `derive` feature)]#derive-pattern-requires-derive-feature
    - [Prompt styling]#prompt-styling-1
    - [Default keybinds]#default-keybinds
  - [Design]#design
    - [Headless mode]#headless-mode
    - [REPL Console]#repl-console
    - [Command parsing]#command-parsing
    - [Scheduling]#scheduling
    - [Directly modifying the terminal (to-do)]#directly-modifying-the-terminal-to-do
    - [Prompt styling]#prompt-styling-2
  - [Known issues \& limitations](#known-issues--limitations)
    - [Built-in `help` and `clear` commands are not yet implemented]#built-in-help-and-clear-commands-are-not-yet-implemented
    - [Runtime toggle is not supported]#runtime-toggle-is-not-supported
    - [Key events are not forwarded to Bevy]#key-events-are-not-forwarded-to-bevy
    - [Minimal renderer prompt does not scroll with terminal output]#minimal-renderer-prompt-does-not-scroll-with-terminal-output
    - [Pretty renderer log history doesn't scroll at all]#pretty-renderer-log-history-doesnt-scroll-at-all
    - [Shift+ aren't entered into the buffer]#shift-arent-entered-into-the-buffer
  - [Aspirations]#aspirations
  - [License]#license

## 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.symbol` is 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:

  ```bash
  cargo run --example custom_renderer --features pretty

  ```

- Minimal usage (in your Bevy app):

  ```rust
  use bevy_repl::prompt::renderer::{PromptRenderer, RenderCtx};

  struct MyRenderer;
  impl PromptRenderer for MyRenderer {
      fn render(&self, f: &mut ratatui::Frame<'_>, ctx: &RenderCtx) {
          // draw a simple 1-line prompt in your own style
          // (see examples/custom_renderer.rs for a complete reference)
          let area = bevy_repl::prompt::renderer::helpers::bottom_bar_area(ctx.area, 1);
          let prompt = ctx.prompt.symbol.clone().unwrap_or_default();
          let spans = [ratatui::text::Span::raw(prompt), ratatui::text::Span::raw(&ctx.repl.buffer)];
          f.render_widget(ratatui::widgets::Paragraph::new(ratatui::text::Line::from(spans)), area);
      }
  }

  App::new()
      .add_plugins(ReplPlugins.set(PromptPlugin {
        renderer: MyRenderer,
        ..default()
      }))
      .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::RatatuiPlugins` is added to your app.
  - Using `ReplPlugins` (the default/turnkey group) automatically adds `RatatuiPlugins`, so the REPL renders in the alternate screen via `RatatuiContext`.
  - Using `MinimalReplPlugins` adds some but not all Ratatui Plugins; the prompt renders on the main terminal screen using the fallback `FallbackTerminalContext`.

- __Minimal (no alternate screen, no built-ins)__

  ```rust
  use bevy::{app::ScheduleRunnerPlugin, prelude::*};
  use bevy_repl::plugin::MinimalReplPlugins;
  use std::time::Duration;

  fn main() {
      App::new()
          .add_plugins((
              MinimalPlugins.set(ScheduleRunnerPlugin::run_loop(Duration::from_secs_f64(1.0/60.0))),
              // Minimal REPL: core + prompt + parser; main-screen rendering
              MinimalReplPlugins,
          ))
          // Add your own commands (no built-ins in minimal)
          // .add_repl_command::<YourCommand>()
          // .add_observer(on_your_command)
          .run();
  }
  ```

- __Default/turnkey (alternate screen + built-ins)__

  ```rust
  use bevy::{app::ScheduleRunnerPlugin, prelude::*};
  use bevy_repl::plugin::ReplPlugins;
  use std::time::Duration;

  fn main() {
      App::new()
          .add_plugins((
              MinimalPlugins.set(ScheduleRunnerPlugin::run_loop(Duration::from_secs_f64(1.0/60.0))),
              // Default REPL: adds RatatuiPlugins + minimal stack + built-ins
              ReplPlugins,
          ))
          .run();
  }
  ```

- __How to choose__
  - Choose `MinimalReplPlugins` if you:
    - Want to stay on the main terminal screen (no full TUI/pane UX).
    - Intend to manage `bevy_ratatui` or other input/render stacks yourself.
    - Prefer to opt-in to commands individually (no built-ins by default).
  - Choose `ReplPlugins` if 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 `RatatuiPlugins` manually.

  By default `ReplPlugins` uses the minimal prompt renderer. To enable the pretty renderer when the `pretty` feature is on, use `ReplPlugins.set(PromptPlugin::pretty())`.

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

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

```rust
fn on_ping(_trigger: Trigger<PingCommand>) {
    bevy_repl::repl_println!("Pong");
}

fn instructions() {
    bevy_repl::repl_println!();
    bevy_repl::repl_println!("Welcome to the Bevy REPL!");
}
```

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

- __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`

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

```rust
use bevy::prelude::*;
use bevy_repl::prelude::*;

fn main() {
    // 1) Install REPL-aware fmt layer before plugins
    tracing_to_repl_fmt();

    App::new()
        .add_plugins((
            // 2) Disable Bevy's stdout logger to prevent duplicate/garbled output
            DefaultPlugins.build().disable::<bevy::log::LogPlugin>(),
            ReplPlugins.set(PromptPlugin::pretty()),
        ))
        .run();
}
```

### 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 `ScrollRegionReadySet` to order your startup prints. This label exists in all builds; in minimal mode it’s a no-op.

```rust
use bevy::prelude::*;
use bevy_repl::prelude::*;

fn instructions() {
    bevy_repl::repl_println!("Welcome!");
}

fn main() {
    App::new()
        .add_plugins(ReplPlugins)
        .add_systems(PostStartup, instructions.after(ScrollRegionReadySet))
        .run();
}
```

## 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)


1. 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.
2. Implement the `ReplCommand` trait for the event struct.
   1. `fn clap_command() -> clap::Command` - Use the `clap` builder pattern to
      describe the command and its arguments or options.
   2. `fn to_event(matches: &clap::ArgMatches) -> ReplResult<Self>` - Implement
      the `to_event` method 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, return `Ok(Self)`.
      **Tip:** If the command has no arguments or options, implement the `Default`
      trait. You don't implement `to_event` in this case, since the default
      implementation will return `Ok(Self)`.
3. Add the command to the app with `.add_repl_command<YourReplCommand>()`.
4. 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.

```rust
use bevy::prelude::*;
use bevy_repl::prelude::*;

#[derive(Debug, Clone, Event, Default)]

struct SimpleCommandWithoutArgs;

impl ReplCommand for SimpleCommandWithoutArgs {
    fn clap_command() -> clap::Command {
        clap::Command::new("simple")
            .about("A simple command")
    }
}

fn on_simple(_trigger: Trigger<SimpleCommandWithoutArgs>) {
    println!("You triggered a simple command without args");
}

struct CommandWithArgs {
    arg1: String,
    arg2: String,
}

impl ReplCommand for CommandWithArgs {

    fn clap_command() -> clap::Command {
        clap::Command::new("command")
            .about("A command with args")
            .arg(clap::Arg::new("arg1").required(true))
            .arg(clap::Arg::new("arg2").required(true))
    }

    fn to_event(matches: &clap::ArgMatches) -> ReplResult<Self> {
        Ok(CommandWithArgs {
            arg1: matches.get_one::<String>("arg1").unwrap().clone(),
            arg2: matches.get_one::<String>("arg2").unwrap().clone(),
        })
    }
}

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 (alternate screen, built-ins) with minimal renderer
            ReplPlugins,
        ))
        .add_repl_command::<SimpleCommandWithoutArgs>()
        .add_observer(on_simple)
        .add_repl_command::<CommandWithArgs>()
        .add_observer(on_command_with_args)
        .run();
}
```

### Derive pattern (requires `derive` feature)


Enable the `derive` feature in your `Cargo.toml` to use the derive pattern.

```toml
[dependencies]
bevy_repl = { version = "0.3.0", features = ["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.

```rust
use bevy::prelude::*;
use bevy_repl::prelude::*;
use clap::Parser;

#[derive(ReplCommand, Parser, Default, Event)]

struct SimpleCommandWithoutArgs;

#[derive(ReplCommand, Parser, Event, Default)]

#[clap(about = "A command with args")]

struct CommandWithArgs {
    #[clap(short, long)]
    arg1: String,
    #[clap(short, long)]
    arg2: String,
}
```

### Prompt styling


- __Appearance__
  - Without the `pretty` feature (default): minimal prompt. One-line bar fixed to the bottom, showing only the prompt symbol and input buffer. No border, colors, or hint.
  - With the `pretty` feature: enhanced prompt. Optional border with title, colored styles for title/prompt/hints, and a right-aligned usage hint.

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

- __Configuration__
  - The prompt is configured via `ReplPromptConfig`. In minimal builds, only the `symbol` is honored; styling options are ignored.
  - In pretty builds, you can use presets or customize:

    ```rust
    // 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(bevy_repl::prompt::ReplPromptConfig::pretty());
    // or
    app.insert_resource(bevy_repl::prompt::ReplPromptConfig::minimal());
    // or explicit fields (pretty build):
    app.insert_resource(bevy_repl::prompt::ReplPromptConfig {
        symbol: Some("> ".to_string()),
        border: Some(bevy_repl::prompt::PromptBorderConfig::default()),
        color: Some(bevy_repl::prompt::PromptColorConfig::default()),
        hint: Some(bevy_repl::prompt::PromptHintConfig::default()),
    });
    ```

  - To run the pretty example:
    ```bash
    cargo run --example pretty --features pretty

    ```

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

["Headless" mode]:
    https://github.com/bevyengine/bevy/blob/main/examples/app/headless.rs

```toml
[dependencies]
bevy = { version = "*", default-features = false }
# replace "*" with the most recent version of bevy
```

```rust
fn main() {
    let mut app = App::new();

    // Run in headless mode at 60 fps
    app.add_plugins((
        MinimalPlugins,
        bevy::app::ScheduleRunnerPlugin::run_loop(
            std::time::Duration::from_secs_f64(1.0 / 60.0),
        )
    ));

    // Exit with Ctrl+C
    app.run();
}
```

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

```toml
[dependencies]
bevy_repl = { version = "0.3.0", features = ["default-commands"] }
```

**REPL disabled (regular headless mode):**

```text
┌───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:**

```text
┌───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.

[builder pattern]: https://docs.rs/clap/latest/clap/_tutorial/index.html#tutorial-for-the-builder-api

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

### Scheduling


The REPL reads input events and emits trigger events alongside the `bevy_ratatui`
[input handling system set](https://github.com/cxreiff/bevy_ratatui/blob/main/src/crossterm_context/event.rs).
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`:

    ```rust
    app.add_plugins(PromptMinimalPlugin);
    ```

- __Pretty__ (feature-gated): border, colorful title/prompt, right-aligned hint.
  - Enable feature and run:

    ```bash
    cargo run --example pretty --features pretty

    ```

  - When the `pretty` feature is enabled, `ReplPlugins` uses the pretty preset automatically. You can still override visuals by inserting `ReplPromptConfig` at runtime.

Advanced users can customize visuals via the `ReplPromptConfig` resource:

```rust
// Use presets
app.insert_resource(ReplPromptConfig::pretty());
// or
app.insert_resource(ReplPromptConfig::minimal());

// Or customize explicitly
app.insert_resource(ReplPromptConfig { border: true, color: false, hint: true });
```
## 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.

```rust
App::new()
    .add_plugins((
        MinimalPlugins.set(ScheduleRunnerPlugin::run_loop(Duration::from_secs_f64(1.0/60.0))),
        // Minimal REPL: core + prompt + parser; main-screen rendering
        MinimalReplPlugins,
    ))
    .add_systems(Update, your_event_reader_system.before(bevy_repl::ReplSet::Pre))
    .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+<Char> 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

- [x] **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.
- [x] **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.
- [x] **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]LICENSE-MIT or
  [http://opensource.org/licenses/MIT]http://opensource.org/licenses/MIT)
- Apache License, Version 2.0 ([LICENSE-APACHE]LICENSE-APACHE or
  [http://www.apache.org/licenses/LICENSE-2.0]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](https://github.com/bevyengine/bevy/issues/2373) to
include both.