diff --git a/Cargo.lock b/Cargo.lock index 6eb56b5..8b138cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4861,6 +4861,7 @@ dependencies = [ "cosmic-text 0.16.0", "q_test_harness", "regex", + "serde", "shlex", "smallvec", "smol_str", diff --git a/crates/cmd_prompt/Cargo.toml b/crates/cmd_prompt/Cargo.toml index 3623f30..1878509 100644 --- a/crates/cmd_prompt/Cargo.toml +++ b/crates/cmd_prompt/Cargo.toml @@ -18,6 +18,7 @@ strum = { version = "0.27.2", features = ["derive"] } thiserror = "2.0.17" tiny_bail = "0.7.0" variadics_please = "1.1.0" +serde = { version = "1.0.228", features = ["derive"] } [features] dylib= ["bevy/dynamic_linking", "dep:bevy_dylib"] diff --git a/crates/cmd_prompt/examples/minimal.rs b/crates/cmd_prompt/examples/minimal.rs index b744287..f352429 100644 --- a/crates/cmd_prompt/examples/minimal.rs +++ b/crates/cmd_prompt/examples/minimal.rs @@ -1,6 +1,6 @@ use bevy::prelude::*; +use bevy_inspector_egui::{bevy_egui::EguiPlugin, quick::WorldInspectorPlugin}; use q_cmd_prompt::prelude::*; -// use bevy_inspector_egui::{bevy_egui::EguiPlugin, quick::WorldInspectorPlugin}; pub fn main() { let mut app = App::new(); @@ -11,8 +11,8 @@ pub fn main() { }), ..Default::default() })); - // app.add_plugins(EguiPlugin::default()); - // app.add_plugins(WorldInspectorPlugin::default()); + app.add_plugins(EguiPlugin::default()); + app.add_plugins(WorldInspectorPlugin::default()); app.add_plugins(ConsolePlugin); app.add_systems(Startup, |mut commands: Commands| { commands.spawn(Camera2d); @@ -22,7 +22,7 @@ pub fn main() { height: Val::Vh(100.), ..Default::default() }, - children![Console], + children![Console::default()], )); }); app.run(); diff --git a/crates/cmd_prompt/examples/styled.rs b/crates/cmd_prompt/examples/styled.rs index c01e142..33cbc91 100644 --- a/crates/cmd_prompt/examples/styled.rs +++ b/crates/cmd_prompt/examples/styled.rs @@ -18,7 +18,7 @@ pub fn main() { ..Default::default() }, children![ - Console, + Console::default(), ConsolePrompt("<=================>\n=>".into()), ConsoleUiSettings { font_color: tailwind::AMBER_700.into(), diff --git a/crates/cmd_prompt/src/actions/actions/basic_input.rs b/crates/cmd_prompt/src/actions/actions/basic_input.rs index a015a03..c5ac2af 100644 --- a/crates/cmd_prompt/src/actions/actions/basic_input.rs +++ b/crates/cmd_prompt/src/actions/actions/basic_input.rs @@ -73,11 +73,18 @@ pub fn submit( mut query: Query<( &mut ConsoleBuffer, &mut ConsoleInputText, - &mut ConsoleHistory, + &ConsoleHistoryHandle, )>, + mut assets: ResMut>, mut commands: Commands, ) { - if let Ok((mut buffer, mut input_text, mut history)) = query.get_mut(input.console_id) { + if let Ok((mut buffer, mut input_text, history_handle)) = query.get_mut(input.console_id) { + let history = assets.get_mut(history_handle.id()); + if history.is_none() { + error!("Failed to get console history!"); + return; + } + let history = history.unwrap(); buffer.write("\n").unwrap(); if let Some(event) = SubmitEvent::new(input.console_id, input_text.text.clone()) { commands.trigger(event); diff --git a/crates/cmd_prompt/src/actions/actions/history.rs b/crates/cmd_prompt/src/actions/actions/history.rs index 43fce56..644d200 100644 --- a/crates/cmd_prompt/src/actions/actions/history.rs +++ b/crates/cmd_prompt/src/actions/actions/history.rs @@ -4,7 +4,8 @@ use crate::prelude::*; pub fn set_from_history( input: In, - mut q_console: Query<(&mut ConsoleInputText, &ConsoleHistory)>, + mut q_console: Query<(&mut ConsoleInputText, &ConsoleHistoryHandle)>, + mut assets: ResMut>, mut history_idx: Local, mut filtered_history: Local>>, mut original_value: Local>, @@ -26,7 +27,10 @@ pub fn set_from_history( _ => {} } if matches!(key, Key::ArrowUp | Key::ArrowDown) { - let (mut input_text, history) = q_console.get_mut(input.console_id).unwrap(); + let (mut input_text, handle) = q_console.get_mut(input.console_id).unwrap(); + let history = assets + .get_mut(handle.id()) + .expect("History asset should exist"); if filtered_history.is_none() { *original_value = Some(std::mem::take(&mut input_text.text)); let f = history @@ -127,11 +131,24 @@ mod test { ); } app.add_step(3, |world: &mut World| { - let console_history = world.query::<&ConsoleHistory>().single_mut(world).unwrap(); - assert_eq!( - **console_history, - vec!["0".to_string(), "1".to_string(), "2".to_string()] - ); + let handle = world + .query::<&ConsoleHistoryHandle>() + .single_mut(world) + .cloned() + .unwrap(); + world.resource_scope::, _>(|world, assets| { + if let Some(history) = assets.get(handle.id()) { + let expected = vec!["0".to_string(), "1".to_string(), "2".to_string()]; + if **history != expected { + error!("History did not match."); + error!(?history); + world.write_message(AppExit::error()); + } + } else { + error!("Failed to get history"); + world.write_message(AppExit::error()); + } + }); world.write_message(key_input( KeyCode::ArrowUp, Key::ArrowUp, diff --git a/crates/cmd_prompt/src/assets.rs b/crates/cmd_prompt/src/assets.rs new file mode 100644 index 0000000..31394e7 --- /dev/null +++ b/crates/cmd_prompt/src/assets.rs @@ -0,0 +1,95 @@ +use bevy::{ + asset::{AssetLoader, AsyncReadExt}, + platform::collections::HashMap, +}; + +use crate::prelude::*; + +/// Environment variables for this console. Saved as '.env' files on disk. +#[derive(Asset, Default, Component, Debug, Deref, DerefMut, Reflect)] +pub struct ConsoleEnvVars(HashMap); + +/// Wrapper for the [Handle] of the [ConsoleEnvVarsAsset] for this [Console] +#[derive(Component, Debug, Deref, DerefMut, Reflect)] +pub struct ConsoleEnvVarsHandle(pub Handle); + +/// Loader for the [ConsoleEnvVarsAsset] +#[derive(Reflect, Default, Debug)] +pub struct ConsoleEnvVarsLoader; +impl AssetLoader for ConsoleEnvVarsLoader { + type Asset = ConsoleEnvVars; + type Settings = (); + type Error = BevyError; + + async fn load( + &self, + reader: &mut dyn bevy::asset::io::Reader, + _settings: &(), + load_context: &mut bevy::asset::LoadContext<'_>, + ) -> Result { + let mut buf = String::new(); + reader.read_to_string(&mut buf).await?; + let map = buf + .split('\n') + .filter_map(|s| { + let mut split = s.split('=').map(|s| s.to_string()).collect::>(); + if split.len() == 2 { + Some((std::mem::take(&mut split[0]), std::mem::take(&mut split[1]))) + } else { + warn!( + "Got invalid line while reading {}:\n'{}'", + load_context.path(), + s + ); + None + } + }) + .collect::>(); + Ok(ConsoleEnvVars(map)) + } + + fn extensions(&self) -> &[&str] { + &["env"] + } +} + +/// Command history of this [Console]. +#[derive(Default, Asset, Debug, Deref, DerefMut, Reflect)] +pub struct ConsoleHistory(Vec); + +/// Wrapper around the [Handle] for the [ConsoleHistoryAsset] for this [Console] +#[derive(Component, Debug, Deref, DerefMut, Reflect, Clone)] +pub struct ConsoleHistoryHandle(pub Handle); + +/// Loader for the [ConsoleHistoryAsset] +#[derive(Reflect, Default, Debug)] +pub struct ConsoleHistoryLoader; +impl AssetLoader for ConsoleHistoryLoader { + type Asset = ConsoleHistory; + type Settings = (); + type Error = BevyError; + + async fn load( + &self, + reader: &mut dyn bevy::asset::io::Reader, + _settings: &(), + _load_context: &mut bevy::asset::LoadContext<'_>, + ) -> Result { + let mut buf = String::new(); + reader.read_to_string(&mut buf).await?; + // todo: not memory efficient + let vec = buf.split('\n').map(|s| s.to_owned()).collect::>(); + Ok(ConsoleHistory(vec)) + } + + fn extensions(&self) -> &[&str] { + &["hist"] + } +} + +pub fn plugin(app: &mut App) { + app.register_asset_loader(ConsoleHistoryLoader); + app.init_asset::(); + app.register_asset_loader(ConsoleEnvVarsLoader); + app.init_asset::(); +} diff --git a/crates/cmd_prompt/src/lib.rs b/crates/cmd_prompt/src/lib.rs index 0964251..6c12d5b 100644 --- a/crates/cmd_prompt/src/lib.rs +++ b/crates/cmd_prompt/src/lib.rs @@ -1,4 +1,5 @@ mod actions; +mod assets; mod commands; mod systems; #[cfg(test)] @@ -8,6 +9,7 @@ mod ui; pub mod prelude { pub use super::ConsolePlugin; pub use super::actions::prelude::*; + pub use super::assets::*; pub use super::commands::prelude::*; pub use super::systems::*; pub use super::ui::prelude::*; @@ -26,6 +28,7 @@ impl Plugin for ConsolePlugin { crate::ui::plugin, crate::commands::plugin, crate::actions::plugin, + crate::assets::plugin, )); app.add_systems( PostUpdate, diff --git a/crates/cmd_prompt/src/test_harness.rs b/crates/cmd_prompt/src/test_harness.rs index 20e0b64..d0dc9eb 100644 --- a/crates/cmd_prompt/src/test_harness.rs +++ b/crates/cmd_prompt/src/test_harness.rs @@ -28,6 +28,6 @@ pub fn plugin(app: &mut App) { } fn setup(mut commands: Commands, mut focus: ResMut) { - let id = commands.spawn(Console).id(); + let id = commands.spawn(Console::new(None, None)).id(); focus.0 = Some(id); } diff --git a/crates/cmd_prompt/src/ui/console.rs b/crates/cmd_prompt/src/ui/console.rs index 5f32c6b..d72b676 100644 --- a/crates/cmd_prompt/src/ui/console.rs +++ b/crates/cmd_prompt/src/ui/console.rs @@ -1,5 +1,6 @@ use crate::prelude::*; use bevy::{ + asset::io::embedded::GetAssetServer, ecs::{lifecycle::HookContext, world::DeferredWorld}, input_focus::InputFocus, }; @@ -12,16 +13,47 @@ use bevy::{ ConsoleBuffer, ConsoleBufferFlags, ConsolePrompt, - ConsoleHistory, ConsoleInputText, TextFont )] #[component(on_add=Self::on_add)] -pub struct Console; +pub struct Console { + /// Path to the history file. If unset, will not serialize. + history_path: Option, + /// Asset ID of the console history. + history: Handle, + /// Path to the environment variables file. If unset, will not serialize. + vars_path: Option, + /// Asset ID of the environment variables asset. + vars: Handle, +} impl Console { + pub fn new(history_path: Option, vars_path: Option) -> Self { + Self { + history_path, + vars_path, + history: Handle::default(), + vars: Handle::default(), + } + } pub(crate) fn on_add<'w>(mut world: DeferredWorld<'w>, ctx: HookContext) { + // load assets + let this = world.get::(ctx.entity).unwrap(); + let server = world.get_asset_server(); + let history = if let Some(path) = this.history_path.as_ref() { + server.load::(path) + } else { + server.add(ConsoleHistory::default()) + }; + let vars = if let Some(path) = this.vars_path.as_ref() { + server.load::(path) + } else { + server.add(ConsoleEnvVars::default()) + }; let bundle = ( Name::new("Console"), + ConsoleHistoryHandle(history), + ConsoleEnvVarsHandle(vars), Node { display: Display::Flex, flex_direction: FlexDirection::ColumnReverse, diff --git a/crates/cmd_prompt/src/ui/data.rs b/crates/cmd_prompt/src/ui/data.rs index a20c3fc..be3336b 100644 --- a/crates/cmd_prompt/src/ui/data.rs +++ b/crates/cmd_prompt/src/ui/data.rs @@ -63,10 +63,6 @@ impl Default for ConsolePrompt { } } -// TODO: Console history should be a file. -#[derive(Component, Debug, Reflect, Clone, Default, Deref, DerefMut)] -pub struct ConsoleHistory(Vec); - #[derive(Component, Debug, Reflect, Clone)] #[component(immutable, on_insert=Self::on_insert)] #[require(Node)]