aboutsummaryrefslogtreecommitdiff
path: root/src/client/cli
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/cli')
-rw-r--r--src/client/cli/cmd/edit.rs53
-rw-r--r--src/client/cli/cmd/mod.rs59
-rw-r--r--src/client/cli/cmd/read.rs55
-rw-r--r--src/client/cli/cmd/write.rs41
-rw-r--r--src/client/cli/mod.rs128
5 files changed, 336 insertions, 0 deletions
diff --git a/src/client/cli/cmd/edit.rs b/src/client/cli/cmd/edit.rs
new file mode 100644
index 0000000..1cfb530
--- /dev/null
+++ b/src/client/cli/cmd/edit.rs
@@ -0,0 +1,53 @@
+//! Replace the contents of the currently edited map with contents from a file.
+
+use super::Command;
+use super::{CmdParseError, FromArgs};
+use crate::client::Editor;
+use crate::net::Cargo;
+use crate::world::World;
+use std::path::PathBuf;
+
+/// Command to load a file from the disk and replace the current editor contents with it's info.
+pub struct Edit {
+ file: PathBuf,
+}
+
+impl FromArgs for Edit {
+ fn from_args(args: &[&str]) -> Result<Self, CmdParseError> {
+ if args.len() != 1 {
+ return Err(CmdParseError::WrongNumberOfArgs(args.len(), 1..=1));
+ }
+
+ Ok(Self {
+ file: PathBuf::from(args[0]),
+ })
+ }
+}
+
+impl Command for Edit {
+ fn process(&self, editor: &mut Editor) -> Result<String, String> {
+ let world = match World::load_from_file(&self.file) {
+ Ok(world) => world,
+ Err(err) => {
+ return Err(format!(
+ "Unable to read file: {:?}, reason: {:?}",
+ &self.file, err
+ ))
+ }
+ };
+
+ // Clear all data from the world, afterwards add all components from the file.
+ editor.server().send(Cargo::ClearAll);
+ for (_, icon) in world.icons().iter() {
+ editor.server().send(Cargo::AddIcon(icon.clone()));
+ }
+ for (_, room) in world.rooms().iter() {
+ editor.server().send(Cargo::AddRoom(room.clone()));
+ }
+ for (_, wall) in world.walls().iter() {
+ editor.server().send(Cargo::AddWall(wall.clone()));
+ }
+
+ Ok(format!("Map data from {:?} loaded.", &self.file))
+ }
+}
diff --git a/src/client/cli/cmd/mod.rs b/src/client/cli/cmd/mod.rs
new file mode 100644
index 0000000..b403772
--- /dev/null
+++ b/src/client/cli/cmd/mod.rs
@@ -0,0 +1,59 @@
+//! The commands that can be performed in the CLI
+
+pub mod edit;
+pub mod read;
+pub mod write;
+
+pub use edit::*;
+pub use read::*;
+pub use write::*;
+
+use crate::client::Editor;
+use std::ops::RangeInclusive;
+
+/// Errors that can occur when parsing a command. This is for syntax checking, the
+/// semantics are checked when trying to execute the command.
+#[allow(missing_docs)]
+#[derive(thiserror::Error, Debug)]
+pub enum CmdParseError {
+ #[error("no command specified")]
+ StringEmpty,
+ #[error("the command {0} is unknown")]
+ NoSuchCmd(String),
+ #[error("wrong number of arguments. Expected in range {1:?}, but received {0}")]
+ WrongNumberOfArgs(usize, RangeInclusive<usize>),
+ #[error("{0} cannot be converted into a {1}, which is required")]
+ InvalidArgType(String, &'static str),
+}
+
+/// Attempts to parse a command from the given string. If it is unsuccessful, it returns a
+/// [CmdParseError].
+pub fn parse_command(string: &str) -> Result<Box<dyn Command>, CmdParseError> {
+ if string.is_empty() {
+ return Err(CmdParseError::StringEmpty);
+ }
+
+ let parts: Vec<&str> = string.split_whitespace().collect();
+ match parts[0] {
+ "w" => Ok(Box::new(Write::from_args(&parts[1..])?)),
+ "e" => Ok(Box::new(Edit::from_args(&parts[1..])?)),
+ "r" => Ok(Box::new(Read::from_args(&parts[1..])?)),
+ other => Err(CmdParseError::NoSuchCmd(other.to_owned())),
+ }
+}
+
+/// Indicates that this entity (command) can be created from arguments. Make sure to check what is
+/// expected, to pass the arguments to the correct command.
+pub trait FromArgs: Sized {
+ /// Creates a new instance from the arguments provided. If for whatever reason the syntax of the
+ /// given arguments is correct an [ArgParseError] is returned.
+ fn from_args(args: &[&str]) -> Result<Self, CmdParseError>;
+}
+
+/// A common trait for all commands.
+pub trait Command {
+ /// Process this command on the provided context. Returns either a string with the output of the
+ /// command when everything went right with it, or an error string explaining what went wrong,
+ /// which can be displayed to the user.
+ fn process(&self, editor: &mut Editor) -> Result<String, String>;
+}
diff --git a/src/client/cli/cmd/read.rs b/src/client/cli/cmd/read.rs
new file mode 100644
index 0000000..3b20308
--- /dev/null
+++ b/src/client/cli/cmd/read.rs
@@ -0,0 +1,55 @@
+//! Read the contents of a file and add it to the currently edited map.
+
+use super::Command;
+use super::{CmdParseError, FromArgs};
+use crate::client::Editor;
+use crate::net::Cargo;
+use crate::world::World;
+use std::path::PathBuf;
+
+/// Command to read a file from the system
+pub struct Read {
+ file: PathBuf,
+}
+
+impl FromArgs for Read {
+ fn from_args(args: &[&str]) -> Result<Self, CmdParseError> {
+ if args.len() != 1 {
+ return Err(CmdParseError::WrongNumberOfArgs(args.len(), 1..=1));
+ }
+
+ Ok(Self {
+ file: PathBuf::from(args[0]),
+ })
+ }
+}
+
+impl Command for Read {
+ fn process(&self, editor: &mut Editor) -> Result<String, String> {
+ let world = match World::load_from_file(&self.file) {
+ Ok(data) => data,
+ Err(err) => {
+ return Err(format!(
+ "Unable to read file: {:?}, reason: {:?}",
+ &self.file, err
+ ))
+ }
+ };
+
+ // Send all components of the file to the server.
+ for (_, icon) in world.icons().iter() {
+ editor.server().send(Cargo::AddIcon(icon.clone()));
+ }
+ for (_, room) in world.rooms().iter() {
+ editor.server().send(Cargo::AddRoom(room.clone()));
+ }
+ for (_, wall) in world.walls().iter() {
+ editor.server().send(Cargo::AddWall(wall.clone()));
+ }
+
+ Ok(format!(
+ "Map data from {:?} read and added to the current buffer.",
+ &self.file
+ ))
+ }
+}
diff --git a/src/client/cli/cmd/write.rs b/src/client/cli/cmd/write.rs
new file mode 100644
index 0000000..37d5a0a
--- /dev/null
+++ b/src/client/cli/cmd/write.rs
@@ -0,0 +1,41 @@
+//! Save the contents of the map to disk
+
+use super::Command;
+use super::{CmdParseError, FromArgs};
+use crate::client::Editor;
+use std::path::PathBuf;
+
+/// The save command can take any destination in the filesystem the user can write to. Processing
+/// will then save the map contents to that destination, overwriting anything that may be there.
+pub struct Write {
+ destination: PathBuf,
+}
+
+impl FromArgs for Write {
+ fn from_args(args: &[&str]) -> Result<Self, CmdParseError> {
+ if args.len() != 1 {
+ return Err(CmdParseError::WrongNumberOfArgs(args.len(), 1..=1));
+ }
+
+ Ok(Self {
+ destination: PathBuf::from(args[0]),
+ })
+ }
+}
+
+impl Command for Write {
+ fn process(&self, editor: &mut Editor) -> Result<String, String> {
+ let world = editor.map().clone_as_world();
+
+ match world.write_to_file(&self.destination) {
+ Ok(_) => Ok(format!(
+ "Successfully wrote contents to `{:?}`",
+ &self.destination
+ )),
+ Err(e) => Err(format!(
+ "Unable to write to `{:?}`. Error: {:?}",
+ &self.destination, e
+ )),
+ }
+ }
+}
diff --git a/src/client/cli/mod.rs b/src/client/cli/mod.rs
new file mode 100644
index 0000000..ed2a1bf
--- /dev/null
+++ b/src/client/cli/mod.rs
@@ -0,0 +1,128 @@
+//! In-window Command line interface. Used for operations that are just easier than with GUI.
+//!
+//! Sometimes it is nice to have a GUI, for instance when a selection has to be made, things have to
+//! be moved etc., however for operations like saving/loading and exporting, no such thing has to be
+//! done and the GUI is really just slowing you down (at least in my opinion). For these operations,
+//! it is much better to simply have a command do that specific thing. It is also much easier to
+//! implement a new command, so features can be tested more quickly. For some things, there should
+//! still be a GUI option. With the example of saving/loading, it is much easier to find some hidden
+//! folder in a GUI, so that is definitely a consideration for the future.
+
+pub mod cmd;
+pub use self::cmd::*;
+
+use crate::client::colours::DEFAULT_COLOURS;
+use crate::client::input::{Button, Input};
+use crate::client::Editor;
+use crate::math::Vec2;
+use raylib::drawing::{RaylibDraw, RaylibDrawHandle};
+use std::sync::mpsc::Receiver;
+
+/// The command line interface. Should be created only once per program instance.
+pub struct CLI {
+ text: String,
+ char_pipe: Option<Receiver<char>>,
+}
+
+impl CLI {
+ /// Create a CLI for this instance
+ pub fn new(input: &mut Input) -> Self {
+ input.add_global(Button::Text(':').into());
+
+ Self {
+ text: String::new(),
+ char_pipe: None,
+ }
+ }
+
+ /// Activates the CLI, which will now capture keyboard input and execute commands accordingly.
+ pub fn try_activate(&mut self, input: &mut Input) {
+ if !self.active() {
+ self.char_pipe = input.try_capture_keyboard();
+
+ if self.char_pipe.is_some() {
+ self.text = ":".to_owned();
+ }
+ }
+ }
+
+ /// Deactivate the command line.
+ pub fn deactivate(&mut self) {
+ // Hang up, the input handler will get the message.
+ self.char_pipe = None;
+ }
+
+ /// Checks if the CLI is currently active. This means input to other things should be ignored.
+ pub fn active(&self) -> bool {
+ self.char_pipe.is_some()
+ }
+
+ fn perform_command(&mut self, editor: &mut Editor) {
+ match cmd::parse_command(&self.text[1..]) {
+ Ok(cmd) => match cmd.process(editor) {
+ Ok(res) => self.text = format!("SUCCESS: {}", res),
+ Err(err) => self.text = format!("ERROR: {}", err),
+ },
+ Err(err) => self.text = format!("SYNTAX ERROR: {}", err),
+ }
+ }
+
+ /// Handle input for the command line and perform any commands the user may want to run.
+ pub fn update(&mut self, editor: &mut Editor, input: &mut Input) {
+ /* Check if the CLI is currently active. If not and it should not be activated according to
+ * keyboard input, there is nothing to do.
+ */
+ if !self.active() {
+ if input.poll_global(&Button::Text(':').into()) {
+ self.try_activate(input);
+ if !self.active() {
+ return;
+ }
+ } else {
+ return;
+ }
+ }
+
+ let rx = self
+ .char_pipe
+ .as_mut()
+ .expect("No character pipe eventhough CLI should be active");
+
+ if let Ok(c) = rx.try_recv() {
+ match c {
+ '\x1B' => self.text.clear(), // Escape
+ '\x7f' => {
+ self.text.pop();
+ } // Backspace
+ '\n' => {
+ self.perform_command(editor);
+ self.deactivate();
+ }
+ c => self.text.push(c),
+ }
+ }
+
+ // When the text is empty, there is also no command marker, so set as inactive and leave.
+ if self.text.is_empty() {
+ self.deactivate()
+ }
+ }
+
+ /// Draw the command line at the bottom of the window.
+ pub fn draw(&self, rld: &mut RaylibDrawHandle) {
+ let pos = Vec2::new(150., rld.get_screen_height() as f32 - 25.);
+
+ rld.draw_rectangle_v(
+ pos,
+ Vec2::new(rld.get_screen_width() as f32 - pos.x, 25.),
+ DEFAULT_COLOURS.cli_background,
+ );
+ rld.draw_text(
+ &self.text,
+ 155,
+ rld.get_screen_height() - 22,
+ 20,
+ DEFAULT_COLOURS.cli_foreground,
+ );
+ }
+}