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}