550 lines
19 KiB
Rust
Executable File
550 lines
19 KiB
Rust
Executable File
use bevy::{
|
|
app::{App, Startup, Update},
|
|
ecs::{
|
|
component::Tick,
|
|
entity::Entity,
|
|
event::{Event, EventReader},
|
|
schedule::IntoSystemConfigs,
|
|
system::{ResMut, Resource, SystemMeta, SystemParam},
|
|
world::{unsafe_world_cell::UnsafeWorldCell, World},
|
|
},
|
|
log::warn,
|
|
};
|
|
// use bevy::{input::keyboard::KeyboardInput, prelude::*};
|
|
// use bevy_egui::egui::{self, Align, ScrollArea, TextEdit};
|
|
// use bevy_egui::egui::{text::LayoutJob, text_edit::CCursorRange};
|
|
// use bevy_egui::egui::{Context, Id};
|
|
// use bevy_egui::{
|
|
// egui::{epaint::text::cursor::CCursor, Color32, FontId, TextFormat},
|
|
// EguiContexts,
|
|
// };
|
|
use clap::{builder::StyledStr, CommandFactory, FromArgMatches};
|
|
use std::collections::BTreeMap;
|
|
use std::marker::PhantomData;
|
|
use std::mem;
|
|
|
|
use crate::{parser, ConsoleSet};
|
|
|
|
type ConsoleCommandEnteredReaderSystemParam = EventReader<'static, 'static, ConsoleCommandEntered>;
|
|
|
|
/// A super-trait for command like structures
|
|
pub trait Command: NamedCommand + CommandFactory + FromArgMatches + Sized + Resource {}
|
|
impl<T: NamedCommand + CommandFactory + FromArgMatches + Sized + Resource> Command for T {}
|
|
|
|
/// Trait used to allow uniquely identifying commands at compile time
|
|
pub trait NamedCommand {
|
|
/// Return the unique command identifier (same as the command "executable")
|
|
fn name() -> &'static str;
|
|
}
|
|
|
|
/// Executed parsed console command.
|
|
///
|
|
/// Used to capture console commands which implement [`CommandName`], [`CommandArgs`] & [`CommandHelp`].
|
|
/// These can be easily implemented with the [`ConsoleCommand`](bevy_console_derive::ConsoleCommand) derive macro.
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```
|
|
/// # use bevy_console::ConsoleCommand;
|
|
/// # use clap::Parser;
|
|
/// /// Prints given arguments to the console.
|
|
/// #[derive(Parser, ConsoleCommand)]
|
|
/// #[command(name = "log")]
|
|
/// struct LogCommand {
|
|
/// /// Message to print
|
|
/// msg: String,
|
|
/// /// Number of times to print message
|
|
/// num: Option<i64>,
|
|
/// }
|
|
///
|
|
/// fn log_command(mut log: ConsoleCommand<LogCommand>) {
|
|
/// if let Some(Ok(LogCommand { msg, num })) = log.take() {
|
|
/// log.ok();
|
|
/// }
|
|
/// }
|
|
/// ```
|
|
pub struct ConsoleCommand<T> {
|
|
command: Option<Result<T, clap::Error>>,
|
|
pub raw: Option<String>,
|
|
pub pilot: Option<Entity>,
|
|
pub operator: Option<Entity>,
|
|
}
|
|
|
|
impl<T> ConsoleCommand<T> {
|
|
/// Returns Some(T) if the command was executed and arguments were valid.
|
|
///
|
|
/// This method should only be called once.
|
|
/// Consecutive calls will return None regardless if the command occurred.
|
|
pub fn take(&mut self) -> Option<Result<T, clap::Error>> {
|
|
mem::take(&mut self.command)
|
|
}
|
|
}
|
|
|
|
pub struct ConsoleCommandState<T> {
|
|
#[allow(clippy::type_complexity)]
|
|
event_reader: <ConsoleCommandEnteredReaderSystemParam as SystemParam>::State,
|
|
marker: PhantomData<T>,
|
|
}
|
|
|
|
unsafe impl<T: Command> SystemParam for ConsoleCommand<T> {
|
|
type State = ConsoleCommandState<T>;
|
|
type Item<'w, 's> = ConsoleCommand<T>;
|
|
|
|
fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State {
|
|
let event_reader = ConsoleCommandEnteredReaderSystemParam::init_state(world, system_meta);
|
|
ConsoleCommandState {
|
|
event_reader,
|
|
marker: PhantomData,
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
unsafe fn get_param<'w, 's>(
|
|
state: &'s mut Self::State,
|
|
system_meta: &SystemMeta,
|
|
world: UnsafeWorldCell<'w>,
|
|
change_tick: Tick,
|
|
) -> Self::Item<'w, 's> {
|
|
let mut event_reader = ConsoleCommandEnteredReaderSystemParam::get_param(
|
|
&mut state.event_reader,
|
|
system_meta,
|
|
world,
|
|
change_tick,
|
|
);
|
|
|
|
let command = event_reader.read().find_map(|command| {
|
|
for name in parser::parse(T::name()) {
|
|
if name == command.command_name {
|
|
let clap_command = T::command().no_binary_name(true);
|
|
let arg_matches = clap_command.try_get_matches_from(command.args.iter());
|
|
|
|
match arg_matches {
|
|
Ok(matches) => match T::from_arg_matches(&matches) {
|
|
Ok(from_arg_matches) => {
|
|
return Some(Ok((
|
|
from_arg_matches,
|
|
command.raw.clone(),
|
|
command.pilot,
|
|
command.operator,
|
|
)));
|
|
}
|
|
Err(err) => return Some(Err(err)),
|
|
},
|
|
Err(err) => {
|
|
return Some(Err(err));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
None
|
|
});
|
|
|
|
if let Some(Ok(command)) = command {
|
|
return ConsoleCommand {
|
|
command: Some(Ok(command.0)),
|
|
raw: Some(command.1),
|
|
pilot: Some(command.2),
|
|
operator: Some(command.3),
|
|
};
|
|
}
|
|
|
|
ConsoleCommand {
|
|
command: None,
|
|
raw: None,
|
|
pilot: None,
|
|
operator: None,
|
|
}
|
|
}
|
|
}
|
|
/// Parsed raw console command into `command` and `args`.
|
|
#[derive(Clone, Debug, Event)]
|
|
pub struct ConsoleCommandEntered {
|
|
/// the command definition
|
|
pub command_name: String,
|
|
/// the raw input string
|
|
pub raw: String,
|
|
/// Raw parsed arguments
|
|
pub args: Vec<String>,
|
|
/// Pilot making the request
|
|
pub pilot: Entity,
|
|
/// Operator responding
|
|
pub operator: Entity,
|
|
}
|
|
|
|
/// Events to print to the console.
|
|
#[derive(Clone, Debug, Eq, Event, PartialEq)]
|
|
pub struct PrintConsoleLine {
|
|
/// Console line
|
|
pub line: StyledStr,
|
|
}
|
|
|
|
impl PrintConsoleLine {
|
|
/// Creates a new console line to print.
|
|
pub const fn new(line: StyledStr) -> Self {
|
|
Self { line }
|
|
}
|
|
}
|
|
|
|
/// Console configuration
|
|
#[derive(Clone, Resource, Default)]
|
|
pub struct ConsoleConfiguration {
|
|
/// Registered console commands
|
|
pub commands: BTreeMap<String, clap::Command>,
|
|
}
|
|
|
|
/// Add a console commands to Bevy app.
|
|
pub trait AddConsoleCommand {
|
|
/// Add a console command with a given system.
|
|
///
|
|
/// This registers the console command so it will print with the built-in `help` console command.
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```
|
|
/// # use bevy::prelude::*;
|
|
/// # use bevy_console::{AddConsoleCommand, ConsoleCommand};
|
|
/// # use clap::Parser;
|
|
/// App::new()
|
|
/// .add_console_command::<LogCommand, _>(log_command);
|
|
/// #
|
|
/// # /// Prints given arguments to the console.
|
|
/// # #[derive(Parser, ConsoleCommand)]
|
|
/// # #[command(name = "log")]
|
|
/// # struct LogCommand;
|
|
/// #
|
|
/// # fn log_command(mut log: ConsoleCommand<LogCommand>) {}
|
|
/// ```
|
|
fn add_console_command<T: Command, Params>(
|
|
&mut self,
|
|
system: impl IntoSystemConfigs<Params>,
|
|
) -> &mut Self;
|
|
}
|
|
|
|
impl AddConsoleCommand for App {
|
|
fn add_console_command<T: Command, Params>(
|
|
&mut self,
|
|
system: impl IntoSystemConfigs<Params>,
|
|
) -> &mut Self {
|
|
let sys = move |mut config: ResMut<ConsoleConfiguration>| {
|
|
for name in crate::parser::parse(T::name()) {
|
|
let command = T::command().no_binary_name(true);
|
|
if config.commands.contains_key(&name) {
|
|
warn!(
|
|
"console command '{}' already registered and was overwritten",
|
|
name
|
|
);
|
|
}
|
|
config.commands.insert(name.clone(), command);
|
|
}
|
|
};
|
|
|
|
self.add_systems(Startup, sys)
|
|
.add_systems(Update, system.in_set(ConsoleSet::Commands))
|
|
}
|
|
}
|
|
|
|
// /// Console open state
|
|
// #[derive(Default, Resource)]
|
|
// pub struct ConsoleOpen {
|
|
// /// Console open
|
|
// pub open: bool,
|
|
// }
|
|
|
|
// #[derive(Resource)]
|
|
// pub(crate) struct ConsoleState {
|
|
// pub(crate) buf: String,
|
|
// pub(crate) scrollback: Vec<StyledStr>,
|
|
// pub(crate) history: VecDeque<StyledStr>,
|
|
// pub(crate) history_index: usize,
|
|
// }
|
|
|
|
// impl Default for ConsoleState {
|
|
// fn default() -> Self {
|
|
// ConsoleState {
|
|
// buf: String::default(),
|
|
// scrollback: Vec::new(),
|
|
// history: VecDeque::from([StyledStr::new()]),
|
|
// history_index: 0,
|
|
// }
|
|
// }
|
|
// }
|
|
|
|
// pub(crate) fn console_ui(
|
|
// mut egui_context: EguiContexts,
|
|
// config: Res<ConsoleConfiguration>,
|
|
// mut keyboard_input_events: EventReader<KeyboardInput>,
|
|
// keys: Res<Input<KeyCode>>,
|
|
// mut state: ResMut<ConsoleState>,
|
|
// mut command_entered: EventWriter<ConsoleCommandEntered>,
|
|
// mut console_open: ResMut<ConsoleOpen>,
|
|
// ) {
|
|
// let keyboard_input_events = keyboard_input_events.iter().collect::<Vec<_>>();
|
|
// let ctx = egui_context.ctx_mut();
|
|
|
|
// let pressed = keyboard_input_events
|
|
// .iter()
|
|
// .any(|code| console_key_pressed(code, &config.keys));
|
|
|
|
// // always close if console open
|
|
// // avoid opening console if typing in another text input
|
|
// if pressed && (console_open.open || !ctx.wants_keyboard_input()) {
|
|
// console_open.open = !console_open.open;
|
|
// }
|
|
|
|
// if console_open.open {
|
|
// egui::Window::new("Console")
|
|
// .collapsible(false)
|
|
// .default_pos([config.left_pos, config.top_pos])
|
|
// .default_size([config.width, config.height])
|
|
// .resizable(true)
|
|
// .show(ctx, |ui| {
|
|
// ui.vertical(|ui| {
|
|
// let scroll_height = ui.available_height() - 30.0;
|
|
|
|
// // Scroll area
|
|
// ScrollArea::vertical()
|
|
// .auto_shrink([false, false])
|
|
// .stick_to_bottom(true)
|
|
// .max_height(scroll_height)
|
|
// .show(ui, |ui| {
|
|
// ui.vertical(|ui| {
|
|
// for line in &state.scrollback {
|
|
// let mut text = LayoutJob::default();
|
|
|
|
// text.append(
|
|
// &line.to_string(), //TOOD: once clap supports custom styling use it here
|
|
// 0f32,
|
|
// TextFormat::simple(FontId::monospace(14f32), Color32::GRAY),
|
|
// );
|
|
|
|
// ui.label(text);
|
|
// }
|
|
// });
|
|
|
|
// // Scroll to bottom if console just opened
|
|
// if console_open.is_changed() {
|
|
// ui.scroll_to_cursor(Some(Align::BOTTOM));
|
|
// }
|
|
// });
|
|
|
|
// // Separator
|
|
// ui.separator();
|
|
|
|
// // Input
|
|
// let text_edit = TextEdit::singleline(&mut state.buf)
|
|
// .desired_width(f32::INFINITY)
|
|
// .lock_focus(true)
|
|
// .font(egui::TextStyle::Monospace);
|
|
|
|
// // Handle enter
|
|
// let text_edit_response = ui.add(text_edit);
|
|
// if text_edit_response.lost_focus()
|
|
// && ui.input(|i| i.key_pressed(egui::Key::Enter))
|
|
// {
|
|
// if state.buf.trim().is_empty() {
|
|
// state.scrollback.push(StyledStr::new());
|
|
// } else {
|
|
// let msg = format!("{}{}", config.symbol, state.buf);
|
|
// state.scrollback.push(msg.into());
|
|
// let cmd_string = state.buf.clone();
|
|
// state.history.insert(1, cmd_string.into());
|
|
// if state.history.len() > config.history_size + 1 {
|
|
// state.history.pop_back();
|
|
// }
|
|
|
|
// let mut args = Shlex::new(&state.buf).collect::<Vec<_>>();
|
|
|
|
// if !args.is_empty() {
|
|
// let command_name = args.remove(0);
|
|
// debug!("Command entered: `{command_name}`, with args: `{args:?}`");
|
|
|
|
// let command = config.commands.get(command_name.as_str());
|
|
|
|
// if command.is_some() {
|
|
// command_entered
|
|
// .send(ConsoleCommandEntered { command_name, args });
|
|
// } else {
|
|
// debug!(
|
|
// "Command not recognized, recognized commands: `{:?}`",
|
|
// config.commands.keys().collect::<Vec<_>>()
|
|
// );
|
|
|
|
// state.scrollback.push("error: Invalid command".into());
|
|
// }
|
|
// }
|
|
|
|
// state.buf.clear();
|
|
// }
|
|
// }
|
|
|
|
// // Clear on ctrl+l
|
|
// if keyboard_input_events
|
|
// .iter()
|
|
// .any(|&k| k.state.is_pressed() && k.key_code == Some(KeyCode::L))
|
|
// && (keys.any_pressed([KeyCode::ControlLeft, KeyCode::ControlRight]))
|
|
// {
|
|
// state.scrollback.clear();
|
|
// }
|
|
|
|
// // Handle up and down through history
|
|
// if text_edit_response.has_focus()
|
|
// && ui.input(|i| i.key_pressed(egui::Key::ArrowUp))
|
|
// && state.history.len() > 1
|
|
// && state.history_index < state.history.len() - 1
|
|
// {
|
|
// if state.history_index == 0 && !state.buf.trim().is_empty() {
|
|
// *state.history.get_mut(0).unwrap() = state.buf.clone().into();
|
|
// }
|
|
|
|
// state.history_index += 1;
|
|
// let previous_item = state.history.get(state.history_index).unwrap().clone();
|
|
// state.buf = previous_item.to_string();
|
|
|
|
// set_cursor_pos(ui.ctx(), text_edit_response.id, state.buf.len());
|
|
// } else if text_edit_response.has_focus()
|
|
// && ui.input(|i| i.key_pressed(egui::Key::ArrowDown))
|
|
// && state.history_index > 0
|
|
// {
|
|
// state.history_index -= 1;
|
|
// let next_item = state.history.get(state.history_index).unwrap().clone();
|
|
// state.buf = next_item.to_string();
|
|
|
|
// set_cursor_pos(ui.ctx(), text_edit_response.id, state.buf.len());
|
|
// }
|
|
|
|
// // Focus on input
|
|
// ui.memory_mut(|m| m.request_focus(text_edit_response.id));
|
|
// });
|
|
// });
|
|
// }
|
|
// }
|
|
|
|
// pub(crate) fn receive_console_line(
|
|
// mut console_state: ResMut<ConsoleState>,
|
|
// mut events: EventReader<PrintConsoleLine>,
|
|
// ) {
|
|
// for event in events.iter() {
|
|
// let event: &PrintConsoleLine = event;
|
|
// console_state.scrollback.push(event.line.clone());
|
|
// }
|
|
// }
|
|
|
|
// fn console_key_pressed(
|
|
// keyboard_input: &KeyboardInput,
|
|
// configured_keys: &[ToggleConsoleKey],
|
|
// ) -> bool {
|
|
// if !keyboard_input.state.is_pressed() {
|
|
// return false;
|
|
// }
|
|
|
|
// for configured_key in configured_keys {
|
|
// match configured_key {
|
|
// ToggleConsoleKey::KeyCode(configured_key_code) => match keyboard_input.key_code {
|
|
// None => continue,
|
|
// Some(pressed_key) => {
|
|
// if configured_key_code == &pressed_key {
|
|
// return true;
|
|
// }
|
|
// }
|
|
// },
|
|
// ToggleConsoleKey::ScanCode(configured_scan_code) => {
|
|
// if &keyboard_input.scan_code == configured_scan_code {
|
|
// return true;
|
|
// }
|
|
// }
|
|
// }
|
|
// }
|
|
|
|
// false
|
|
// }
|
|
|
|
// fn set_cursor_pos(ctx: &Context, id: Id, pos: usize) {
|
|
// if let Some(mut state) = TextEdit::load_state(ctx, id) {
|
|
// state.set_ccursor_range(Some(CCursorRange::one(CCursor::new(pos))));
|
|
// state.store(ctx, id);
|
|
// }
|
|
// }
|
|
|
|
// #[cfg(test)]
|
|
// mod tests {
|
|
// use bevy::input::ButtonState;
|
|
|
|
// use super::*;
|
|
|
|
// #[test]
|
|
// fn test_console_key_pressed_scan_code() {
|
|
// let input = KeyboardInput {
|
|
// scan_code: 41,
|
|
// key_code: None,
|
|
// state: ButtonState::Pressed,
|
|
// window: Entity::PLACEHOLDER,
|
|
// };
|
|
|
|
// let config = vec![ToggleConsoleKey::ScanCode(41)];
|
|
|
|
// let result = console_key_pressed(&input, &config);
|
|
// assert!(result);
|
|
// }
|
|
|
|
// #[test]
|
|
// fn test_console_wrong_key_pressed_scan_code() {
|
|
// let input = KeyboardInput {
|
|
// scan_code: 42,
|
|
// key_code: None,
|
|
// state: ButtonState::Pressed,
|
|
// window: Entity::PLACEHOLDER,
|
|
// };
|
|
|
|
// let config = vec![ToggleConsoleKey::ScanCode(41)];
|
|
|
|
// let result = console_key_pressed(&input, &config);
|
|
// assert!(!result);
|
|
// }
|
|
|
|
// #[test]
|
|
// fn test_console_key_pressed_key_code() {
|
|
// let input = KeyboardInput {
|
|
// scan_code: 0,
|
|
// key_code: Some(KeyCode::Grave),
|
|
// state: ButtonState::Pressed,
|
|
// window: Entity::PLACEHOLDER,
|
|
// };
|
|
|
|
// let config = vec![ToggleConsoleKey::KeyCode(KeyCode::Grave)];
|
|
|
|
// let result = console_key_pressed(&input, &config);
|
|
// assert!(result);
|
|
// }
|
|
|
|
// #[test]
|
|
// fn test_console_wrong_key_pressed_key_code() {
|
|
// let input = KeyboardInput {
|
|
// scan_code: 0,
|
|
// key_code: Some(KeyCode::A),
|
|
// state: ButtonState::Pressed,
|
|
// window: Entity::PLACEHOLDER,
|
|
// };
|
|
|
|
// let config = vec![ToggleConsoleKey::KeyCode(KeyCode::Grave)];
|
|
|
|
// let result = console_key_pressed(&input, &config);
|
|
// assert!(!result);
|
|
// }
|
|
|
|
// #[test]
|
|
// fn test_console_key_right_key_but_not_pressed() {
|
|
// let input = KeyboardInput {
|
|
// scan_code: 0,
|
|
// key_code: Some(KeyCode::Grave),
|
|
// state: ButtonState::Released,
|
|
// window: Entity::PLACEHOLDER,
|
|
// };
|
|
|
|
// let config = vec![ToggleConsoleKey::KeyCode(KeyCode::Grave)];
|
|
|
|
// let result = console_key_pressed(&input, &config);
|
|
// assert!(!result);
|
|
// }
|
|
// }
|