I'm a C/C++ programmer and this is my first Rust program. A heavy simplification of the console system like used in Quake and Source engine games, that lets you set variables and run commands.
I'm aware the parsing algorithm is bad, it's just supposed to be simple. I want to know how I can improve this code to make it less ugly or more idiomatic Rust. I think I'm still writing with a leftover C++ mindset and Rust does not like that. Even for something this simple I had to fight the compiler (see the comment // can't print immediately because of borrowing rules
) over wanting to print a console message in the middle of a console function. (I didn't want to duplicate the code in the message
function, that'd be ugly and wouldn't work in a console with a more complex message
function.)
Usually in my C++ programs I have Console
as just a global state namespace, not an object, but that seems like a bad idea to even try in Rust. However with Console as an object, ConCommand
callbacks aren't able to use the message
method because they have no handle to Console
. Every part of a large game would need a handle to Console
just for debug messages. I'd also like an easier way of registering newly-created variables/commands with the Console
. Is there something better I can do on that front as well?
console.rs:
pub struct ConVar {
pub name: String,
pub value: String,
}
pub struct ConCommand {
pub name: String,
pub callback: fn(args: &[String])
}
pub struct Console {
// line history, for rendering if used in a gui program, or a dump lines to file command
pub history: Vec<String>,
pub commands: Vec<ConCommand>,
pub variables: Vec<ConVar>
}
impl Console {
pub fn new() -> Console {
Console {
history: Vec::new(),
commands: Vec::new(),
variables: Vec::new()
}
}
pub fn print(&mut self, msg: String) {
println!("CONSOLE: {}", msg);
self.history.push(msg);
}
pub fn execute(&mut self, args: &[String]) {
if args.is_empty() {
return;
}
let mut msg = String::new();
for i in self.variables.iter_mut() {
if i.name == args[0] {
if args.len() > 1 {
i.value = args[1].clone();
return;
} else {
// can't print immediately because of borrowing rules
msg = format!("\"{}\" is \"{}\"", i.name, i.value);
break;
}
}
}
if !msg.is_empty() {
self.print(msg);
return;
}
for i in self.commands.iter_mut() {
if i.name == args[0] {
(i.callback)(args);
return;
}
}
self.print(format!("Unknown command \"{}\"", args[0]));
}
pub fn parse(&mut self, str: String) {
if str.is_empty() {
return;
}
let mut args: Vec<String> = Vec::new();
for c in str.chars() {
match c {
'\n' => { continue; }
'\r' => { continue; }
' ' => {
if args.len() > 0 {
args.push(String::from(""));
}
}
_ => {
let index = args.len();
if index > 0 {
if let Some(s) = args.get_mut(index - 1) {
s.push(c);
} else {
args.push(String::from(c));
}
} else {
args.push(String::from(c));
}
}
}
}
args.retain(|s| { !(s.is_empty()) });
if args.len() == 0 {
return;
}
self.execute(args.as_slice());
}
}
main.rs:
mod console;
use console::*;
use std::io::Write;
fn echo(args: &[String]) {
if args.len() < 2 {
println!("Usage: echo <message>");
return;
}
println!("{}", args[1]);
}
fn main() {
let mut con = Console::new();
let cvar = ConVar {
name: "testvar".to_owned(),
value: "default".to_owned(),
};
con.variables.push(cvar);
let cmd = ConCommand {
name: "echo".to_owned(),
callback: echo,
};
con.commands.push(cmd);
loop {
print!(">");
std::io::stdout().flush().unwrap(); // show the prompt
let mut line = String::new();
match std::io::stdin().read_line(&mut line) {
Ok(_) => con.parse(line),
Err(e) => println!("Err: {}", e)
}
}
}