Skip to main content

chill_bevy_console/
lib.rs

1//! A configurable developer console plugin for [Bevy](https://bevyengine.org) games.
2//!
3//! Press `` ` `` (backtick) to toggle the console open and closed. Commands are
4//! plain Bevy systems that take [`CommandArgs`] and return a [`String`].
5//!
6//! # Quickstart
7//!
8//! ```no_run
9//! use bevy::prelude::*;
10//! use chill_bevy_console::{ChillConsole, CommandArgs, ConsoleAppExt, console_closed};
11//!
12//! fn main() {
13//!     App::new()
14//!         .add_plugins(DefaultPlugins)
15//!         .add_plugins(ChillConsole::default())
16//!         .add_console_command("say", "say <text> — echo text", say_cmd)
17//!         .add_systems(Update, gameplay_input.run_if(console_closed))
18//!         .run();
19//! }
20//!
21//! fn say_cmd(In(args): CommandArgs) -> String {
22//!     args.join(" ")
23//! }
24//!
25//! fn gameplay_input() { /* movement, jump, etc. */ }
26//! ```
27//!
28//! # Cargo features
29//!
30//! - `embedded-font` — embed `UbuntuMono-R.ttf` in the binary and use it as the
31//!   default font, so consumers don't need to ship a font asset.
32//! - `persistent-history` — load and save the console's full display history
33//!   (commands and their outputs) to a plain-text file between runs. The path
34//!   is configured via [`ConsoleConfig::history_file`].
35//!
36//! # Customization
37//!
38//! Every visual element is configurable via [`ConsoleConfig`], with built-in
39//! presets ([`ConsoleConfig::chillgames`], [`ConsoleConfig::matrix`],
40//! [`ConsoleConfig::source`]) as starting points. See the
41//! [`USAGE.md`](https://github.com/Chillgames-net/bevy_console/blob/main/USAGE.md)
42//! guide and the [`examples/`](https://github.com/Chillgames-net/bevy_console/tree/main/examples)
43//! directory for runnable demos.
44
45mod args;
46mod commands;
47mod input;
48mod registry;
49mod state;
50mod ui;
51
52#[cfg(feature = "persistent-history")]
53mod persistence;
54
55pub mod config;
56
57pub use args::Args;
58pub use config::ConsoleConfig;
59pub use registry::ConsoleRegistry;
60pub use state::ConsoleState;
61
62#[cfg(feature = "embedded-font")]
63use bevy::asset::uuid_handle;
64use bevy::prelude::*;
65
66// ── Embedded font ──────────────────────────────────────────────────────────────
67
68/// Stable handle for the embedded UbuntuMono font.
69/// Only meaningful when the `embedded-font` feature is enabled.
70#[cfg(feature = "embedded-font")]
71pub const UBUNTU_MONO_FONT_HANDLE: Handle<Font> =
72    uuid_handle!("7fca4e91-3b58-d20a-9c63-e0174f2b85d6");
73
74use input::{
75    capture_console_input, console_open, console_open_and_changed, execute_pending_commands,
76    handle_toggle_key, has_pending_command, scroll_console, sync_console_ui,
77};
78use ui::{ConsoleAssets, update_console_ui};
79
80// ── Command type ───────────────────────────────────────────────────────────────
81
82/// The input type for console command systems.
83///
84/// ```no_run
85/// # use chill_bevy_console::CommandArgs;
86/// # use bevy::prelude::*;
87/// fn say_cmd(In(args): CommandArgs) -> String {
88///     args.rest(0)
89/// }
90/// ```
91pub type CommandArgs = In<Args>;
92
93// ── App extension ──────────────────────────────────────────────────────────────
94
95pub trait ConsoleAppExt {
96    /// Register a Bevy system as a console command.
97    ///
98    /// The system receives the command arguments as `In<Vec<String>>` and must
99    /// return a `String` (the output shown in the console, or empty for no output).
100    ///
101    /// ```no_run
102    /// # use chill_bevy_console::{CommandArgs, ConsoleAppExt};
103    /// # use bevy::prelude::*;
104    /// fn say_cmd(In(args): CommandArgs) -> String {
105    ///     args.join(" ")
106    /// }
107    /// # let mut app = App::new();
108    /// app.add_console_command("say", "say <text> — echo text", say_cmd);
109    /// ```
110    fn add_console_command<M>(
111        &mut self,
112        name: &'static str,
113        usage: &'static str,
114        system: impl IntoSystem<In<Args>, String, M> + 'static,
115    ) -> &mut Self;
116}
117
118impl ConsoleAppExt for App {
119    fn add_console_command<M>(
120        &mut self,
121        name: &'static str,
122        usage: &'static str,
123        system: impl IntoSystem<In<Args>, String, M> + 'static,
124    ) -> &mut Self {
125        self.init_resource::<ConsoleRegistry>();
126        let system_id = self.world_mut().register_system(system);
127        self.world_mut()
128            .resource_mut::<ConsoleRegistry>()
129            .register(name, usage, system_id);
130        self
131    }
132}
133
134// ── Run condition ──────────────────────────────────────────────────────────────
135
136/// Returns `true` when the console is **closed**.
137///
138/// Use this as a run condition to suppress gameplay input while the console is open:
139///
140/// ```no_run
141/// # use bevy::prelude::*;
142/// # use chill_bevy_console::console_closed;
143/// # fn handle_movement() {}
144/// # let mut app = App::new();
145/// app.add_systems(Update, handle_movement.run_if(console_closed));
146/// ```
147pub fn console_closed(state: Option<Res<ConsoleState>>) -> bool {
148    state.map_or(true, |s| !s.open)
149}
150
151// ── Plugin ─────────────────────────────────────────────────────────────────────
152
153/// The main plugin.
154///
155/// ```no_run
156/// # use bevy::prelude::*;
157/// # use chill_bevy_console::{ChillConsole, ConsoleConfig};
158/// # let mut app = App::new();
159/// app.add_plugins(ChillConsole::default());
160///
161/// // Or with custom config:
162/// # let mut app = App::new();
163/// app.add_plugins(ChillConsole {
164///     config: ConsoleConfig {
165///         input_border_color: Color::srgb(0.2, 0.8, 0.4),
166///         toggle_key: KeyCode::F1,
167///         ..default()
168///     },
169/// });
170/// ```
171pub struct ChillConsole {
172    pub config: ConsoleConfig,
173}
174
175impl Default for ChillConsole {
176    fn default() -> Self {
177        Self {
178            config: ConsoleConfig::default(),
179        }
180    }
181}
182
183impl Plugin for ChillConsole {
184    fn build(&self, app: &mut App) {
185        // Embed font bytes into Assets<Font> before ConsoleAssets is initialized,
186        // so that FromWorld can resolve UBUNTU_MONO_FONT_HANDLE immediately.
187        #[cfg(feature = "embedded-font")]
188        {
189            let bytes = include_bytes!(concat!(
190                env!("CARGO_MANIFEST_DIR"),
191                "/assets/UbuntuMono-R.ttf"
192            ));
193            let font = Font::try_from_bytes(bytes.to_vec()).expect("embedded font is valid");
194            let _ = app
195                .world_mut()
196                .resource_mut::<Assets<Font>>()
197                .insert(&UBUNTU_MONO_FONT_HANDLE, font);
198        }
199
200        #[cfg(feature = "persistent-history")]
201        let initial_state = persistence::load_initial_state(&self.config);
202        #[cfg(not(feature = "persistent-history"))]
203        let initial_state = ConsoleState::default();
204
205        app.insert_resource(self.config.clone())
206            .init_resource::<ConsoleRegistry>()
207            .insert_resource(initial_state)
208            .init_resource::<ConsoleAssets>()
209            .add_plugins(commands::plugin)
210            .add_systems(
211                Update,
212                (
213                    handle_toggle_key,
214                    sync_console_ui,
215                    capture_console_input.run_if(console_open),
216                    scroll_console.run_if(console_open),
217                    execute_pending_commands.run_if(has_pending_command),
218                    update_console_ui.run_if(console_open_and_changed),
219                )
220                    .chain(),
221            );
222
223        #[cfg(feature = "persistent-history")]
224        app.add_plugins(persistence::plugin);
225    }
226}