quakeball-rust/rust/src/ui/cli.rs
2026-07-16 07:17:07 +03:00

215 lines
6.4 KiB
Rust

use std::collections::HashMap;
use std::net::SocketAddr;
use std::ops::DerefMut;
use std::rc::Rc;
use godot::classes::{Engine, IPanel, Panel, RichTextLabel, ScrollContainer, TextEdit};
use godot::prelude::*;
use godot::tools::get_autoload_by_name;
pub enum CliColor {
Command,
Info,
Help,
Error,
}
impl CliColor {
pub fn as_str(&self) -> &str {
match self {
CliColor::Command => "#a2a2a2",
CliColor::Info => "#fff",
CliColor::Help => "#ffdc2e",
CliColor::Error => "#cc3535",
}
}
}
pub const CLI_GLOBAL_NAME: &str = "CommandLinePanelGlobal";
#[derive(GodotClass)]
#[class(base=Panel)]
pub struct CommandLinePanel {
#[export]
pub input_field: Option<Gd<TextEdit>>,
#[export]
pub text_field: Option<Gd<RichTextLabel>>,
#[export]
pub scroll_container: Option<Gd<ScrollContainer>>,
scroll_next_update: bool,
commands: HashMap<String, Rc<Box<dyn Command>>>,
base: Base<Panel>,
}
#[godot_api]
impl IPanel for CommandLinePanel {
fn init(base: Base<Panel>) -> Self {
let mut commands: HashMap<String, Rc<Box<dyn Command>>> = HashMap::new();
commands.insert("help".to_owned(), Rc::new(Box::new(HelpCommand)));
commands.insert("host".to_string(), Rc::new(Box::new(HostCommand)));
commands.insert("join".to_string(), Rc::new(Box::new(JoinCommand)));
commands.insert(
"disconnect".to_string(),
Rc::new(Box::new(DisconnectCommand)),
);
CommandLinePanel {
input_field: None,
text_field: None,
scroll_container: None,
scroll_next_update: false,
commands,
base,
}
}
fn process(&mut self, _delta: f64) {
if let Some(scroll) = &mut self.scroll_container
&& self.scroll_next_update
{
scroll.set_v_scroll(i32::MAX);
self.scroll_next_update = false;
}
if let Some(input) = self.input_field.clone() {
let text = input.get_text();
if text.contains("\n") {
let parts = text.split("\n");
let first = parts.get(0).unwrap();
self.input_command(first.to_string());
}
}
}
}
#[godot_api]
impl CommandLinePanel {
#[signal]
pub fn on_host(port: u16);
#[signal]
pub fn on_join(port: String);
#[signal]
pub fn on_disconnect();
pub fn singleton() -> Gd<CommandLinePanel> {
get_autoload_by_name::<CommandLinePanel>(CLI_GLOBAL_NAME)
}
pub fn input_command(&mut self, command: String) {
self.publish_message(format!("> {}", command), CliColor::Command);
if let Some(input) = &mut self.input_field {
input.clear();
}
self.handle_command(&command);
}
pub fn publish_message(&mut self, message: String, color: CliColor) {
if let Some(field) = &mut self.text_field {
if !field.get_parsed_text().is_empty() {
field.append_text("\n");
}
field.append_text(&format!("[color={}]", color.as_str()));
field.append_text(&message);
field.append_text("[/color]");
self.scroll_next_update = true;
}
}
fn handle_command(&mut self, text: &String) {
let mut command_and_params = text.split(" ").into_iter();
let command = command_and_params.next().clone().unwrap_or("");
let params = command_and_params.collect::<Vec<_>>();
if let Some(command) = self.commands.get(command) {
command.clone().execute(self, &params);
} else {
self.publish_message(format!("Unknown command: {}", command), CliColor::Info);
}
}
}
trait Command {
fn execute(&self, cli: &mut CommandLinePanel, params: &[&str]);
fn help(&self) -> &str;
}
#[derive(Clone)]
struct HelpCommand;
impl Command for HelpCommand {
fn execute(&self, cli: &mut CommandLinePanel, params: &[&str]) {
if let Some(command_name) = params.first() {
if let Some(command) = cli.commands.get(*command_name) {
cli.publish_message(command.help().to_string(), CliColor::Help);
} else {
cli.publish_message(format!("Unknown command: {}", command_name), CliColor::Help);
}
} else {
let mut keys = cli.commands.keys().cloned().collect::<Vec<_>>();
keys.sort();
for key in keys {
cli.publish_message(
format!("\t- {}: {}", key, cli.commands.get(&key).unwrap().help()),
CliColor::Help,
);
}
}
}
fn help(&self) -> &str {
"Shows this message"
}
}
#[derive(Clone)]
struct HostCommand;
impl Command for HostCommand {
fn execute(&self, cli: &mut CommandLinePanel, params: &[&str]) {
if let Some(port_str) = params.first() {
if let Ok(port) = u16::from_str_radix(*port_str, 10) {
cli.signals().on_host().emit(port);
} else {
cli.publish_message(format!("Invalid port: {}", port_str), CliColor::Error);
}
} else {
cli.publish_message(format!("Missing port"), CliColor::Error);
cli.publish_message(format!("Usage: host [port]"), CliColor::Help);
}
}
fn help(&self) -> &str {
"Hosts a server at a given port"
}
}
#[derive(Clone)]
struct JoinCommand;
impl Command for JoinCommand {
fn execute(&self, cli: &mut CommandLinePanel, params: &[&str]) {
if let Some(address_str) = params.first() {
if let Ok(_) = address_str.parse::<SocketAddr>() {
cli.signals().on_join().emit(address_str.to_string());
} else {
cli.publish_message(format!("Invalid address: {}", address_str), CliColor::Error);
}
} else {
cli.publish_message(format!("Missing address"), CliColor::Error);
cli.publish_message(format!("Usage: join [address]:[port]"), CliColor::Help);
}
}
fn help(&self) -> &str {
"Joins server at a given address:port"
}
}
#[derive(Clone)]
struct DisconnectCommand;
impl Command for DisconnectCommand {
fn execute(&self, cli: &mut CommandLinePanel, _params: &[&str]) {
cli.signals().on_disconnect().emit();
}
fn help(&self) -> &str {
"Disconnects from the server, or stops hosting the server"
}
}