diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index b1400b1..0a8fbc6 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -1,18 +1,18 @@ name: Changelog on: - # pull_request: - # branches: - # - "main" - # paths-ignore: # don't trigger on changes to these files - # - "CHANGELOG.md" - # push: - # branches: - # - "main" - # paths-ignore: # don't trigger on changes to these files - # - "CHANGELOG.md" - workflow_dispatch: - + pull_request: + branches: + - "main" + paths-ignore: # don't trigger on changes to these files + - "CHANGELOG.md" + push: + tags: + - "*" + branches-ignore: # don't trigger on changes to these files + - "CHANGELOG.md" + workflow_dispatch: {} + jobs: update-changelog: diff --git a/.github/workflows/mdbook.yml b/.github/workflows/mdbook.yml new file mode 100644 index 0000000..8e5abd6 --- /dev/null +++ b/.github/workflows/mdbook.yml @@ -0,0 +1,54 @@ +name: Deploy mdBook to GitHub Pages + +on: + pull_request: + push: + tags: [ "v*" ] + workflow_dispatch: {} + +permissions: + contents: write + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Cache Cargo binaries and registry + uses: actions/cache@v4 + with: + path: | + ~/.cargo/bin + ~/.cargo/registry + ~/.cargo/git + key: ${{ runner.os }}-cargo-tools-${{ hashFiles('**/book.toml') }} + restore-keys: | + ${{ runner.os }}-cargo-tools- + + - name: Setup mdBook + uses: peaceiris/actions-mdbook@v1 + with: + mdbook-version: 'latest' + + - name: Install mdBook Table of Contents plugin + uses: baptiste0928/cargo-install@v3 + with: + crate: mdbook-toc + + - name: Install mdBook alerts plugin + uses: baptiste0928/cargo-install@v3 + with: + crate: mdbook-alerts + + - name: Build book + run: mdbook build + + - name: Deploy to gh-pages + uses: peaceiris/actions-gh-pages@v4 + if: startsWith(github.ref, 'refs/tags/') + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_branch: gh-pages + publish_dir: doc/book/ diff --git a/.gitignore b/.gitignore index 118f254..d008ece 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,6 @@ .cargo/config.toml .editorconfig Cargo.lock + +# Ignore mdBook build output but keep sources +doc/book/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 3490616..7a94242 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,16 +1,22 @@ # Changelog -Notable changes to this project will be documented in this file. -## [unreleased] +See [GitHub Releases](https://github.com/philiplinden/bevy_repl/releases) and the [Bevy REPL Book](https://philiplinden.github.io/bevy_repl) for details. + +## [0.3.0] - 2025-08-14 ### 📚 Documentation +- Update readme ([`a958b70`](https://github.com/philiplinden/bevy_repl/commit/a958b705327bd2623a7b98854ec84197ab1d9808)) +- Add demo tape to readme ([`ce5a626`](https://github.com/philiplinden/bevy_repl/commit/ce5a62600c6dcc7c4af8d240fc202b7a00da6f06)) +- Readability updates ([`55104ba`](https://github.com/philiplinden/bevy_repl/commit/55104ba0625ef91d54c16136e52e053d9abf7283)) +- Fancy demo ([`a328508`](https://github.com/philiplinden/bevy_repl/commit/a328508202d64ad721aca2f60fd4186e978e0c38)) +- Limitations ([`69b39fb`](https://github.com/philiplinden/bevy_repl/commit/69b39fb3109362e2fb6593032870d4884126fa1b)) - Add known issues to readme ([`3a7dbe1`](https://github.com/philiplinden/bevy_repl/commit/3a7dbe1c7eb931a82a4b755f797e2d92aaea7bef)) -- Update readme ([`2c677ea`](https://github.com/philiplinden/bevy_repl/commit/2c677eaccfe6626643f34878005a44c9e3c387ed)) ### 🐛 Bug Fixes +- Disable the changelog for now ([`ca1d368`](https://github.com/philiplinden/bevy_repl/commit/ca1d36876c9188de896cfc04990fd0ad7d3f6bf8)) - Clean up examples ([`3f6f234`](https://github.com/philiplinden/bevy_repl/commit/3f6f23478cb7fbd01e5b454cc2b9fa93857ff8d5)) - Bad plugin setup ([`a1ebbe2`](https://github.com/philiplinden/bevy_repl/commit/a1ebbe262f5e09933b2085c6acf93c0ff20bbb93)) - Close and quit were misconfigured ([`df104b1`](https://github.com/philiplinden/bevy_repl/commit/df104b1c2d681092473b18b247078bd3cd033c64)) @@ -18,6 +24,13 @@ Notable changes to this project will be documented in this file. ### 🧪 Experimental +- Disable changelog on main for now ([`9b02990`](https://github.com/philiplinden/bevy_repl/commit/9b02990de3259c45000b042907dcee764eb7ad8f)) +- Demo ([`a1ff6b3`](https://github.com/philiplinden/bevy_repl/commit/a1ff6b35814d12e86fea38dd553c1ff1e7a432c0)) +- Custom log layer ([`bd50098`](https://github.com/philiplinden/bevy_repl/commit/bd5009857148a6e4d3016c2539a1213aef5e0776)) +- Pretty renderer is working ([`3cd37cd`](https://github.com/philiplinden/bevy_repl/commit/3cd37cd56beeb075ec2ef06ffba426b960b3c3b1)) +- Repl println macro ([`a7bb78f`](https://github.com/philiplinden/bevy_repl/commit/a7bb78fe151ce3e30f634d93e67bb122611e465b)) +- Lots of examples and ergonomic renderer settings ([`7e3cbee`](https://github.com/philiplinden/bevy_repl/commit/7e3cbee95dc8c65a2238c4f4056c618dd125e835)) +- Custom renderer ([`ca17531`](https://github.com/philiplinden/bevy_repl/commit/ca17531f035b85ef7ff265ad74815acee74b532d)) - Even more examples ([`e70cf98`](https://github.com/philiplinden/bevy_repl/commit/e70cf9878b71bdbe277cfddfa1c6b983c0b28bf5)) - Overhaul ([`d0bbea2`](https://github.com/philiplinden/bevy_repl/commit/d0bbea2136678e53368615042bd024a6e4b8e075)) - Works with windowed apps too! ([`955262b`](https://github.com/philiplinden/bevy_repl/commit/955262b2e3b1df9d111bc8fc3851bc3f19fb2c06)) @@ -37,7 +50,7 @@ Notable changes to this project will be documented in this file. - Add rustyline, write out design docs ([`fd4ea0f`](https://github.com/philiplinden/bevy_repl/commit/fd4ea0f9ed8be49b0333468f540dbebf32a5fa49)) -## New Contributors ❤️ +### New Contributors ❤️ -* @philiplinden made their first contribution in [#5](https://github.com/philiplinden/bevy_repl/pull/5) +* @philiplinden made their first contribution diff --git a/Cargo.toml b/Cargo.toml index 0ec014e..973fa5e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = [".", "bevy_repl_derive"] [package] name = "bevy_repl" -version = "0.4.0" +version = "0.4.1" edition = "2024" description = "Add a REPL to headless Bevy applications" license = "MIT" @@ -15,7 +15,7 @@ categories = ["game-development", "development-tools"] bevy = { version = "0.16.1", default-features = false, features = ["bevy_log", "trace"]} bevy_ratatui = { version = "0.9.2", features = ["crossterm"] } ratatui = "0.29.0" -clap = { version = "4.5.45" } +clap = { version = "4.5.46" } shell-words = "1.1" bevy_repl_derive = { version = "0.1.0", path = "./bevy_repl_derive", optional = true } ctrlc = "3" @@ -34,11 +34,10 @@ default = [ ] dev = ["bevy/dynamic_linking"] derive = ["bevy_repl_derive", "clap/derive"] -stdout = [] -default_commands = ["quit"] +default_commands = ["quit", "help"] quit = [] -clear = [] help = [] +clear = [] [[example]] name = "default" diff --git a/README.md b/README.md index 1226e65..d216ed7 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,12 @@ Bevy application runs, allowing users to enter commands and interact with the ECS at runtime. ```rust -use bevy_repl::ReplPlugins; -App::new().add_plugins(ReplPlugins); +use bevy::prelude::*; +use bevy_repl::prelude::*; + +fn main() { + App::new().add_plugins((DefaultPlugins, ReplPlugins)); +} ``` ![Made with VHS](https://vhs.charm.sh/vhs-6kUt4mnyvUcmbpVfWzHx4s.gif) @@ -33,47 +37,14 @@ The REPL is designed as an alternative to 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) - - [Versions](#versions) - - [Features](#features) - - [Derive](#derive) - - [Built-in commands](#built-in-commands) - - [Prompt styling](#prompt-styling) - - [Routing Bevy logs to the REPL](#routing-bevy-logs-to-the-repl) - - [Usage](#usage) - - [Builder pattern (default)](#builder-pattern-default) - - [Derive pattern (requires `derive` feature)](#derive-pattern-requires-derive-feature) - - [Default keybinds](#default-keybinds) - - [Intro to Bevy REPL](#intro-to-bevy-repl) - - [Headless mode](#headless-mode) - - [Headless app with default features except windowing](#headless-app-with-default-features-except-windowing) - - [Minimal headless app (no default features)](#minimal-headless-app-no-default-features) - - [Headless app with default features and windowing disabled](#headless-app-with-default-features-and-windowing-disabled) - - [Command parsing](#command-parsing) - - [Scheduling](#scheduling) - - [Prompt styling](#prompt-styling-1) - - [Terminal Screens](#terminal-screens) - - [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 while the REPL is enabled](#key-events-are-not-forwarded-to-bevy-while-the-repl-is-enabled) - - [Renderer prompt buffer does not scroll with terminal output](#renderer-prompt-buffer-does-not-scroll-with-terminal-output) - - [Shift+ aren't entered into the buffer](#shift-arent-entered-into-the-buffer) - - [Directly modifying the terminal is buggy](#directly-modifying-the-terminal-is-buggy) - - [Aspirations](#aspirations) - - [License](#license) +_This is my first public Bevy plugin, and I vibe-coded a large part of it._ +**_You have been warned_.** -## Versions - -| Version | Bevy | Ratatui | Notes | -| --- | --- | --- | --- | -| 0.4.0 | 0.16.1 | 0.29 | Removed the "pretty" renderer in favor of getting simple prompt features working. Changed the interface slightly. This is a breaking change! See [examples](https://github.com/philiplinden/bevy_repl/tree/main/examples) for help. | -| 0.3.0 | 0.16.1 | 0.29 | 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. | +| Version | Bevy | Notes | +| --- | --- | --- | +| 0.4.1 | 0.16.1 | Better docs: [philiplinden.github.io/bevy_repl](https://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](https://github.com/philiplinden/bevy_repl/tree/main/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 @@ -85,616 +56,66 @@ additional features at your own risk. | --- | --- | --- | | `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` | -### 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 | -| --- | --- | --- | --- | -| `quit` | `quit`, `q`, `exit` | Gracefully terminate the application | `true` | - -### 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. - -```rust -use bevy::prelude::*; - -fn main() { - App::new() - .add_plugins(( - DefaultPlugins, - bevy_repl::ReplPlugins, - )) - .run(); -} -``` - -### Routing Bevy logs to the REPL - -You can optionally route logs produced by Bevy's `tracing` pipeline to the REPL -so they are formatted in the REPL's renderer. Otherwise, `bevy::log::LogPlugin` -will print logs directly to stdout. This means that if you are using an -alternate TUI screen (like with the default `RatatuiPlugins`), Bevy log messages -will not be visible in the REPL unless you disable Bevy's `LogPlugin`. - -When the default `LogPlugin` is disabled, 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`. -- You can then read `Event` yourself, or use the provided system that - prints via `repl_println!` so lines render above the prompt. - -```rust -use bevy::prelude::*; -use bevy_repl::prelude::*; - -fn main() { - App::new() - .add_plugins(( - // 2) Disable Bevy's stdout logger to prevent duplicate/garbled output - DefaultPlugins.build().disable::(), - ReplPlugins, - )) - .run(); -} -``` - -## 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. - -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. - -It is not possible to toggle the REPL on and off at runtime (yet!). - -### 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` - 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()`. -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 CommandWithoutArgs; - -impl ReplCommand for CommandWithoutArgs { - fn clap_command() -> clap::Command { - clap::Command::new("command") - .about("A command without args") - } -} - -fn on_command_without_args(_trigger: Trigger) { - println!("You triggered a command without args"); -} - -#[derive(Debug, Clone, Event, Default)] -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 { - Ok(CommandWithArgs { - arg1: matches.get_one::("arg1").unwrap().clone(), - arg2: matches.get_one::("arg2").unwrap().clone(), - }) - } -} - -fn on_command_with_args(trigger: Trigger) { - 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::() - .add_observer(on_command_without_args) - .add_repl_command::() - .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. +## Batteries-included setup ```toml [dependencies] -bevy_repl = { version = "0.3.1", features = ["derive"] } +bevy = "0.16.1" +bevy_repl = { version = "0.4.1", default-features = true } ``` -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. - -```rust -use bevy::prelude::*; -use bevy_repl::prelude::*; -use clap::Parser; - -#[derive(ReplCommand, Parser, Default, Event)] -struct CommandWithoutArgs; +**Optional features:** -fn on_command_without_args(_trigger: Trigger) { - println!("You triggered a command without args"); -} +| Feature Flag | Description | Default | +| --- | --- | --- | +| `derive` | Support clap's derive pattern for REPL commands | `false` | +| `default_commands` | Enable all built-in commands | `true` | -#[derive(ReplCommand, Parser, Event, Default)] -#[clap(about = "A command with args")] -struct CommandWithArgs { - #[clap(short, long)] - arg1: String, - #[clap(short, long)] - arg2: String, -} +### ReplPlugins -fn on_command_with_args(trigger: Trigger) { - println!("You triggered a command with args: {} {}", trigger.arg1, trigger.arg2); -} +```rust +use bevy::prelude::*; +use bevy_repl::ReplPlugins; fn main() { App::new() + // Headless with a stable frame time (60 FPS) - this is important! .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) + DefaultPlugins + .set(bevy::app::ScheduleRunnerPlugin::run_loop( + std::time::Duration::from_secs_f32(1.0/60.0) + )) + // disable `LogPlugin` to route Bevy logs into the REPL renderer + .build().disable::(), ReplPlugins, )) - .add_repl_command::() - .add_observer(on_command_without_args) - .add_repl_command::() - .add_observer(on_command_with_args) .run(); } ``` -### 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 | -| `Ctrl+C` | Terminate app | - -**Note:** `Ctrl+C` behaves like a normal terminal interrupt and terminates the -Bevy app. The REPL plugin is set up with some safety net event handling to -ensure that the terminal state is restored back to normal upon exiting the app, -but it is possible that the terminal state is not restored if the app is -terminated abnormally. When this happens, restart your terminal to restore the -terminal state back to normal. - -## Intro to Bevy REPL - -### 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.) - -```toml -[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 - ] -} -``` - -```rust -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. - -```rust -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. - -```rust -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::(), - // 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 +### ReplCommand 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()`. 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. - -[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::() - .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 { - Ok(SayCommand { - message: matches.get_all::("message").unwrap().join(" "), - }) - } -} - -fn on_say(trigger: Trigger) { - 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`. - -### Prompt styling - -The REPL prompt supports basic configuration via the `ReplPromptConfig` resource. - -You can configure the prompt symbol: - -```rust -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](examples/custom_renderer.rs). - -### Terminal Screens - -Ratatui TUIs create an "alternate screen" by default, which is a -terminal canvas separate from stdout buffer. This means Ratatui TUIs have -complete control over what is rendered to the entire terminal, but the displayed -output does not persist after the app is closed. - -When the REPL is active, the terminal runs in raw mode by default, so the output -is printed to stdout. 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!`. - -`repl_println!` moves the cursor to column 0 before printing, writes CRLF -(`\r\n`), and flushes stdout. - -```rust -fn on_ping(_trigger: Trigger) { - 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!`. - -## 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 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 _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))), - ReplPlugins, - )) - .add_systems(Update, your_event_reader_system.before(bevy_repl::ReplSet::Pre)) - .run(); -``` - -### Renderer prompt buffer does not scroll with terminal output -The prompt is rendered in the terminal below the normal stdout, but the entire -buffered input 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. - -### Shift+ aren't entered into the buffer -`Shift + lowercase letter` is ignored by the prompt. This is because the prompt -captures only characters, not chords. Since shift is a modifier, extra logic is -needed to support it. This is not implemented yet. - -### Directly modifying the terminal is buggy -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. +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::()`. +3. Handle the command event with an observer: `.add_observer(on_command)`. -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. +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. -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. +## Bevy REPL Book -## Aspirations -- [x] **Derive pattern** - Describe commands with clap's derive pattern. -- [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. -- [x] **Printing to stdout** - 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** - 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 terminal 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** - 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. +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](https://philiplinden.github.io/bevy_repl/). ## License diff --git a/book.toml b/book.toml new file mode 100644 index 0000000..87f0208 --- /dev/null +++ b/book.toml @@ -0,0 +1,27 @@ +[book] +title = "Bevy REPL Book" +authors = ["Philip Linden"] +language = "en" +multilingual = false +src = "doc/src" + +[build] +build-dir = "doc/book/" +create-missing = false + +[preprocessor.toc] +command = "mdbook-toc" +renderer = ["html"] +max-level = 3 + +[preprocessor.alerts] + +[output.html] +# Link back to the repository +git-repository-url = "https://github.com/philiplinden/bevy_repl" +# Needed so relative links work when hosted at https://.github.io/bevy_repl/ +site-url = "/bevy_repl/" + +[output.html.fold] +enable = false # whether or not to enable section folding +level = 1 # the depth to start folding diff --git a/cliff.toml b/cliff.toml index 504e1be..d74e777 100644 --- a/cliff.toml +++ b/cliff.toml @@ -1,31 +1,20 @@ # git-cliff ~ configuration file # https://git-cliff.org/docs/configuration -# [remote.github] -# owner = "philiplinden" -# repo = "buoy" +[remote.github] +owner = "philiplinden" +repo = "bevy_repl" [workspace] -# Path to your workspace Cargo.toml manifest_path = "Cargo.toml" -[bump] -# Bump minor version for new features -features_always_bump_minor = true -# Bump major version for breaking changes -breaking_always_bump_major = true -# Initial version for new repositories -initial_tag = "0.1.0" - [changelog] -# A Tera template to be rendered as the changelog's footer. -# See https://keats.github.io/tera/docs/#introduction header = """ # Changelog\n -Notable changes to this project will be documented in this file.\n + +See [GitHub Releases](https://github.com/philiplinden/bevy_repl/releases) and the [Bevy REPL Book](https://philiplinden.github.io/bevy_repl) for details. + """ -# A Tera template to be rendered for each release in the changelog. -# See https://keats.github.io/tera/docs/#introduction body = """ {%- macro remote_url() -%} https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }} @@ -49,7 +38,7 @@ body = """ ## [unreleased] {% endif %}\ -{% for group, commits in commits | group_by(attribute="group") %} +{% for group, commits in commits | unique(attribute="message") | group_by(attribute="group") %} ### {{ group | striptags | trim | upper_first }} {% for commit in commits | filter(attribute="scope") @@ -65,7 +54,7 @@ body = """ {%- if github -%} {% if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %} -## New Contributors ❤️ +### New Contributors ❤️ {% endif %}\ {% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %} * @{{ contributor.username }} made their first contribution @@ -77,8 +66,6 @@ body = """ """ -# A Tera template to be rendered as the changelog's footer. -# See https://keats.github.io/tera/docs/#introduction footer = """ """ @@ -89,7 +76,7 @@ render_always = false # An array of regex based postprocessors to modify the changelog. postprocessors = [ # Replace the placeholder with a URL. - { pattern = '', replace = "https://github.com/philiplinden/buoy" }, + { pattern = '', replace = "https://github.com/philiplinden/bevy_repl" }, ] # render body even when there are no releases to process # render_always = true @@ -138,14 +125,14 @@ commit_parsers = [ # Exclude commits that are not matched by any commit parser. filter_commits = true # Regex to select git tags that represent releases. -tag_pattern = "[0-9].*" +tag_pattern = "v[0-9].*" # Regex to select git tags that do not represent proper releases. # Takes precedence over `tag_pattern`. # Changes belonging to these releases will be included in the next release. skip_tags = "beta|alpha" # Regex to exclude git tags after applying the tag_pattern. -ignore_tags = [] +# ignore_tags = [] # Order releases topologically instead of chronologically. topo_order = true # Order of commits in each group/release within the changelog. diff --git a/doc/src/SUMMARY.md b/doc/src/SUMMARY.md new file mode 100644 index 0000000..da88b18 --- /dev/null +++ b/doc/src/SUMMARY.md @@ -0,0 +1,22 @@ +# Summary + +[Quickstart](index.md) + +- [Features](features/index.md) + - [Derive](features/derive.md) + - [Default Commands](features/default_commands.md) +- [Commands](commands/index.md) + - [Builder Pattern](commands/builder_pattern.md) + - [Derive Pattern](commands/derive_pattern.md) +- [Configuration](config/index.md) + - [Keybinds](config/keybinds.md) + - [Prompt Styling](config/prompt_styling.md) +- [Design](design/index.md) + - [Command Parsing](design/command_parsing.md) + - [Logging](design/logging.md) + - [Scheduling](design/scheduling.md) + - [Terminal Screens](design/terminal_screens.md) +- [Development](dev/index.md) + - [Known Issues & Limitations](dev/known_issues.md) + - [Changelog](dev/changelog.md) + - [License](dev/license.md) diff --git a/doc/src/commands/builder_pattern.md b/doc/src/commands/builder_pattern.md new file mode 100644 index 0000000..c9b762d --- /dev/null +++ b/doc/src/commands/builder_pattern.md @@ -0,0 +1,67 @@ +# 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()`. 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. + +[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::() + .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 { + Ok(SayCommand { + message: matches.get_all::("message").unwrap().join(" "), + }) + } +} + +fn on_say(trigger: Trigger) { + println!("{}", trigger.message); +} +``` diff --git a/doc/src/commands/derive_pattern.md b/doc/src/commands/derive_pattern.md new file mode 100644 index 0000000..f44d038 --- /dev/null +++ b/doc/src/commands/derive_pattern.md @@ -0,0 +1,61 @@ +# Derive pattern (requires `derive` feature) + +Enable the `derive` feature in your `Cargo.toml` to use the derive pattern. + +**Example**: +[derive.rs](https://github.com/philiplinden/bevy_repl/blob/main/examples/derive.rs) + +```toml +[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. + +```rust +use bevy::prelude::*; +use bevy_repl::prelude::*; +use clap::Parser; + +#[derive(ReplCommand, Parser, Default, Event)] +struct CommandWithoutArgs; + +fn on_command_without_args(_trigger: Trigger) { + 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) { + 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::() + .add_observer(on_command_without_args) + .add_repl_command::() + .add_observer(on_command_with_args) + .run(); +} +``` diff --git a/doc/src/commands/index.md b/doc/src/commands/index.md new file mode 100644 index 0000000..82d317d --- /dev/null +++ b/doc/src/commands/index.md @@ -0,0 +1,19 @@ +# 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()`. 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](https://bevyengine.org/examples/ecs-entity-component-system/observers/). diff --git a/doc/src/config/index.md b/doc/src/config/index.md new file mode 100644 index 0000000..3ad1c21 --- /dev/null +++ b/doc/src/config/index.md @@ -0,0 +1,7 @@ +# 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. diff --git a/doc/src/config/keybinds.md b/doc/src/config/keybinds.md new file mode 100644 index 0000000..b9aa545 --- /dev/null +++ b/doc/src/config/keybinds.md @@ -0,0 +1,23 @@ +# Keybinds + +The following keybinds control the REPL's input buffer cursor. + +| 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 | + +> [!WARNING] +> Ctrl+C behaves like a normal terminal interrupt because Bevy REPL +> implements a hook to handle `SIGINT` (Ctrl+C) interrupts in addition to Bevy's +> `AppExit` event to restore the terminal state (disable "raw mode") on exit. This +> is baked into the REPL plugin and doesn't require any additional setup, so +> Ctrl+C still works even if the built-in quit command is disabled. + +Keybinds for the input buffer are not yet customizable (_see +[Known Issues & Limitations](../dev/known_issues.md)_). diff --git a/doc/src/config/prompt_styling.md b/doc/src/config/prompt_styling.md new file mode 100644 index 0000000..0f3c5c3 --- /dev/null +++ b/doc/src/config/prompt_styling.md @@ -0,0 +1,39 @@ +# 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. + +```rust +use bevy::prelude::*; + +fn main() { + App::new() + .add_plugins(( + DefaultPlugins, + bevy_ratatui::RatatuiPlugins::default(), + bevy_repl::ReplPlugins, + )) + .run(); +} +``` + +**Example**: +[alt_screen.rs](https://github.com/philiplinden/bevy_repl/blob/main/examples/alt_screen.rs) + +The REPL prompt supports basic configuration via the `ReplPromptConfig` resource. + +You can configure the prompt symbol: + +```rust +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](examples/custom_renderer.rs). diff --git a/doc/src/design/command_parsing.md b/doc/src/design/command_parsing.md new file mode 100644 index 0000000..a44749f --- /dev/null +++ b/doc/src/design/command_parsing.md @@ -0,0 +1,29 @@ +# Command Parsing + +Input is parsed via `clap` commands and corresponding observer systems that +execute when triggered by the REPL. + + + +## Minimal example (builder pattern) + +```rust +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 { + Ok(Say { msg: m.get_one::("msg").unwrap().clone() }) + } +} + +fn on_say(t: Trigger) { println!("{}", t.msg); } +``` + +See `examples/` for more. diff --git a/doc/src/design/index.md b/doc/src/design/index.md new file mode 100644 index 0000000..4463801 --- /dev/null +++ b/doc/src/design/index.md @@ -0,0 +1,138 @@ +# 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.) + +```toml +[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 + ] +} +``` + +```rust +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. + +```rust +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. + +```rust +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::(), + // 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(); +} +``` diff --git a/doc/src/design/logging.md b/doc/src/design/logging.md new file mode 100644 index 0000000..55d6bb6 --- /dev/null +++ b/doc/src/design/logging.md @@ -0,0 +1,30 @@ +# Routing Bevy logs to the REPL + +You can optionally route logs produced by Bevy's `tracing` pipeline to the REPL +so they are formatted in the REPL's renderer. Otherwise, `bevy::log::LogPlugin` +will print logs directly to stdout. This means that if you are using an +alternate TUI screen (like with the default `RatatuiPlugins`), Bevy log messages +will not be visible in the REPL unless you disable Bevy's `LogPlugin`. + +When the default `LogPlugin` is disabled, 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`. +- You can then read `Event` yourself, or use the provided system that + prints via `repl_println!` so lines render above the prompt. + +```rust +use bevy::prelude::*; +use bevy_repl::prelude::*; + +fn main() { + App::new() + .add_plugins(( + // 2) Disable Bevy's stdout logger to prevent duplicate/garbled output + DefaultPlugins.build().disable::(), + ReplPlugins, + )) + .run(); +} +``` diff --git a/doc/src/design/scheduling.md b/doc/src/design/scheduling.md new file mode 100644 index 0000000..74ef273 --- /dev/null +++ b/doc/src/design/scheduling.md @@ -0,0 +1,15 @@ +# 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`. diff --git a/doc/src/design/terminal_screens.md b/doc/src/design/terminal_screens.md new file mode 100644 index 0000000..ad88d70 --- /dev/null +++ b/doc/src/design/terminal_screens.md @@ -0,0 +1,20 @@ +# 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: + +```rust +fn on_ping(_t: Trigger) { + bevy_repl::repl_println!("Pong"); +} + +fn instructions() { + bevy_repl::repl_println!(); + bevy_repl::repl_println!("Welcome to the Bevy REPL!"); +} +``` diff --git a/doc/src/dev/changelog.md b/doc/src/dev/changelog.md new file mode 100644 index 0000000..c2cb8bd --- /dev/null +++ b/doc/src/dev/changelog.md @@ -0,0 +1,7 @@ +# Changelog + + + +{{#include ../../../README.md:39:43}} + +{{#include ../../../CHANGELOG.md:4:}} diff --git a/doc/src/dev/index.md b/doc/src/dev/index.md new file mode 100644 index 0000000..ba26640 --- /dev/null +++ b/doc/src/dev/index.md @@ -0,0 +1,33 @@ +# Development + +This chapter contains developer notes, to-dos, known issues, and other +information for those who want to contribute to the crate. + +## Aspirations + +- [x] **Derive pattern** - Describe commands with clap's derive pattern. +- [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. +- [x] **Printing to stdout** - 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** - 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 terminal 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** - 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. diff --git a/doc/src/dev/known_issues.md b/doc/src/dev/known_issues.md new file mode 100644 index 0000000..7f0e015 --- /dev/null +++ b/doc/src/dev/known_issues.md @@ -0,0 +1,90 @@ +# Known Issues & Limitations + + + +Known rough edges and limitations (see README for latest details): + +- Built-in `help` and `clear` commands are not yet implemented. +- Runtime toggle is not supported. +- Key events are not forwarded to Bevy while the REPL is enabled. +- Renderer prompt buffer does not scroll with terminal output. +- Shift+ aren't entered into the buffer. +- 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. + +## 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 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 _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))), + ReplPlugins, + )) + .add_systems(Update, your_event_reader_system.before(bevy_repl::ReplSet::Pre)) + .run(); +``` + +## Renderer prompt buffer does not scroll with terminal output +The prompt is rendered in the terminal below the normal stdout, but the entire +buffered input 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. + +## Shift+ aren't entered into the buffer +`Shift + lowercase letter` is ignored by the prompt. This is because the prompt +captures only characters, not chords. Since shift is a modifier, extra logic is +needed to support it. This is not implemented yet. + +## Directly modifying the terminal is buggy +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. diff --git a/doc/src/dev/license.md b/doc/src/dev/license.md new file mode 100644 index 0000000..552ca8f --- /dev/null +++ b/doc/src/dev/license.md @@ -0,0 +1,11 @@ +# License + +**This project is dual-licensed under Apache-2.0 or MIT, at your option.** + +## Apache-2.0 + +{{#include ../../../LICENSE-APACHE }} + +## MIT + +{{#include ../../../LICENSE-MIT }} diff --git a/doc/src/features/default_commands.md b/doc/src/features/default_commands.md new file mode 100644 index 0000000..ed04adf --- /dev/null +++ b/doc/src/features/default_commands.md @@ -0,0 +1,44 @@ +# 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) | `quit`, `q`, `exit` | Gracefully terminate the application | `quit` | `true` | +| [help](#help) | `help` | Show available commands | `help` | `true` | +| [clear](#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) diff --git a/doc/src/features/derive.md b/doc/src/features/derive.md new file mode 100644 index 0000000..c3599ea --- /dev/null +++ b/doc/src/features/derive.md @@ -0,0 +1,37 @@ +# 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)]`. + +```toml +[dependencies] +bevy_repl = { version = "0.4", features = ["derive"] } +``` + +```rust +use bevy::prelude::*; +use bevy_repl::prelude::*; +use clap::Parser; + +#[derive(ReplCommand, Parser, Default, Event)] +struct Ping; + +fn on_ping(_t: Trigger) { + println!("pong"); +} + +fn main() { + App::new() + .add_plugins(( + MinimalPlugins, + bevy::input::InputPlugin::default(), + ReplPlugins, + )) + .add_repl_command::() + .add_observer(on_ping) + .run(); +} +``` diff --git a/doc/src/features/index.md b/doc/src/features/index.md new file mode 100644 index 0000000..a6dcfa4 --- /dev/null +++ b/doc/src/features/index.md @@ -0,0 +1,5 @@ +# Features + + + +{{#include ../../../README.md:47:58}} diff --git a/doc/src/index.md b/doc/src/index.md new file mode 100644 index 0000000..032b23f --- /dev/null +++ b/doc/src/index.md @@ -0,0 +1,5 @@ +{{#include ../../README.md}} + +## Default keybinds + +{{#include config/keybinds.md:2:}} diff --git a/examples/builtins.rs b/examples/builtins.rs index a9cb5bc..800cd6d 100644 --- a/examples/builtins.rs +++ b/examples/builtins.rs @@ -35,22 +35,16 @@ fn instructions() { repl_println!("This example shows how built-in commands are compiled via features."); repl_println!("Available built-ins at compile-time:"); - // Report which built-ins were compiled in - repl_println!(" help : {}", yes_no(cfg!(feature = "help"))); - repl_println!(" clear: {}", yes_no(cfg!(feature = "clear"))); - repl_println!(" quit : {}", yes_no(cfg!(feature = "quit"))); - repl_println!(); repl_println!("Try these commands in the REPL:"); - repl_println!(" help - Show available commands (if enabled)"); - repl_println!(" clear - Clear the REPL screen (if enabled)"); - repl_println!(" quit - Exit the app (if enabled)"); + repl_println!(" quit - Exit the app ({})", yes_no(cfg!(feature = "quit"))); + repl_println!(" help - Show available commands ({})", yes_no(cfg!(feature = "help"))); + repl_println!(" clear - Clear the screen ({})", yes_no(cfg!(feature = "clear"))); repl_println!(); repl_println!("To control which are enabled, run with Cargo feature flags."); repl_println!("Examples:"); repl_println!(" cargo run --example builtins"); repl_println!(" cargo run --example builtins --no-default-features --features \"help\""); - repl_println!(" cargo run --example builtins --no-default-features --features \"quit,clear\""); repl_println!(); repl_println!("Press CTRL+C to exit any time."); repl_println!(); diff --git a/src/built_ins/help.rs b/src/built_ins/help.rs index 4ff462d..1477699 100644 --- a/src/built_ins/help.rs +++ b/src/built_ins/help.rs @@ -1,5 +1,6 @@ use bevy::prelude::*; use crate::prelude::*; +use crate::repl_println; pub fn plugin(app: &mut App) { app.add_repl_command::(); @@ -16,5 +17,5 @@ impl crate::command::ReplCommand for HelpCommand { } fn on_help(_t: Trigger) { - error!("Help command not yet implemented."); + repl_println!("not implemented, sorry"); } diff --git a/src/built_ins/quit.rs b/src/built_ins/quit.rs index 262ed89..40afd9c 100644 --- a/src/built_ins/quit.rs +++ b/src/built_ins/quit.rs @@ -1,5 +1,5 @@ -use bevy::prelude::*; use crate::prelude::*; +use bevy::prelude::*; pub fn plugin(app: &mut App) { app.add_repl_command::(); @@ -11,7 +11,9 @@ struct QuitCommand; impl crate::command::ReplCommand for QuitCommand { fn clap_command() -> clap::Command { - clap::Command::new("quit").alias("q").alias("exit") + clap::Command::new("quit") + .visible_alias("q") + .visible_alias("exit") .about("Exits the app gracefully") } } diff --git a/src/command/register.rs b/src/command/register.rs index 4d7363c..3dd6a14 100644 --- a/src/command/register.rs +++ b/src/command/register.rs @@ -1,6 +1,7 @@ use bevy::prelude::*; use crate::repl::Repl; use super::{ReplCommand, TypedCommandParser, CommandParser}; + /// Extension trait for App to add REPL commands pub trait ReplAppExt { /// Add a REPL command with its observer function @@ -33,4 +34,4 @@ pub fn register_command_in_repl(mut repl: ResMut) { Box::new(TypedCommandParser::::new()) as Box, ); } -} \ No newline at end of file +} diff --git a/src/lib.rs b/src/lib.rs index 26fa09f..6ed763a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,9 +24,6 @@ pub mod print; pub mod prompt; pub mod repl; -#[cfg(feature = "stdout")] -pub mod stdout; - pub mod prelude { pub use crate::built_ins::ReplDefaultCommandsPlugin; #[cfg(not(feature = "derive"))]