Initial Commit
This commit is contained in:
554
crates/guardian_commands/src/console.rs
Normal file
554
crates/guardian_commands/src/console.rs
Normal file
@@ -0,0 +1,554 @@
|
||||
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::{debug, 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());
|
||||
|
||||
debug!(
|
||||
"Trying to parse as `{}`. Result: {arg_matches:?}",
|
||||
command.command_name
|
||||
);
|
||||
|
||||
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);
|
||||
// }
|
||||
// }
|
||||
Reference in New Issue
Block a user