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 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, /// } /// /// fn log_command(mut log: ConsoleCommand) { /// if let Some(Ok(LogCommand { msg, num })) = log.take() { /// log.ok(); /// } /// } /// ``` pub struct ConsoleCommand { command: Option>, pub raw: Option, pub pilot: Option, pub operator: Option, } impl ConsoleCommand { /// 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> { mem::take(&mut self.command) } } pub struct ConsoleCommandState { #[allow(clippy::type_complexity)] event_reader: ::State, marker: PhantomData, } unsafe impl SystemParam for ConsoleCommand { type State = ConsoleCommandState; type Item<'w, 's> = ConsoleCommand; 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, /// 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, } /// 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::(log_command); /// # /// # /// Prints given arguments to the console. /// # #[derive(Parser, ConsoleCommand)] /// # #[command(name = "log")] /// # struct LogCommand; /// # /// # fn log_command(mut log: ConsoleCommand) {} /// ``` fn add_console_command( &mut self, system: impl IntoSystemConfigs, ) -> &mut Self; } impl AddConsoleCommand for App { fn add_console_command( &mut self, system: impl IntoSystemConfigs, ) -> &mut Self { let sys = move |mut config: ResMut| { 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, // pub(crate) history: VecDeque, // 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, // mut keyboard_input_events: EventReader, // keys: Res>, // mut state: ResMut, // mut command_entered: EventWriter, // mut console_open: ResMut, // ) { // let keyboard_input_events = keyboard_input_events.iter().collect::>(); // 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::>(); // 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::>() // ); // 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, // mut events: EventReader, // ) { // 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); // } // }