futriix-cli/src/main.rs

148 lines
4.2 KiB
Rust
Raw Normal View History

2025-05-25 14:35:35 +00:00
use std::io::{self, Write};
2025-05-25 21:28:01 +00:00
use std::net::TcpStream;
2025-05-25 14:35:35 +00:00
use std::time::Duration;
use std::env;
2025-05-25 21:28:01 +00:00
use colored::Colorize;
2025-05-25 14:35:35 +00:00
use regex::Regex;
2025-05-25 21:28:01 +00:00
use std::process;
2025-05-25 14:35:35 +00:00
mod resp;
const DEFAULT_PORT: u16 = 9880;
const PROMPT_NAME: &str = "futriix";
2025-05-25 21:28:01 +00:00
const CONNECTION_TIMEOUT_SECS: u64 = 2;
2025-05-25 14:35:35 +00:00
fn main() {
2025-05-25 21:28:01 +00:00
let (host, port) = parse_args();
let addr = format!("{}:{}", host, port);
let stream = match TcpStream::connect_timeout(
&addr.parse().unwrap(),
Duration::from_secs(CONNECTION_TIMEOUT_SECS)
) {
Ok(stream) => stream,
Err(e) if e.kind() == io::ErrorKind::ConnectionRefused => {
eprintln!("{}", "Connection refused".red());
process::exit(1);
},
Err(e) => {
eprintln!("Connection error: {}", e);
process::exit(1);
}
};
println!("Connected to {}", addr.green());
run_repl_loop(stream, &host, port);
}
fn parse_args() -> (String, u16) {
2025-05-25 14:35:35 +00:00
let args: Vec<String> = env::args().collect();
let mut host = "127.0.0.1".to_string();
let mut port = DEFAULT_PORT;
if args.len() > 1 {
let re = Regex::new(r"^(?:([^:]+):)?([^:]+)(?::(\d+))?$").unwrap();
if let Some(caps) = re.captures(&args[1]) {
if let Some(h) = caps.get(2) {
host = h.as_str().to_string();
}
if let Some(p) = caps.get(3) {
port = p.as_str().parse().unwrap_or(DEFAULT_PORT);
}
}
}
2025-05-25 21:28:01 +00:00
(host, port)
}
2025-05-25 14:35:35 +00:00
2025-05-25 21:28:01 +00:00
fn run_repl_loop(stream: TcpStream, host: &str, port: u16) {
2025-05-25 14:35:35 +00:00
let mut input = String::new();
loop {
2025-05-25 21:28:01 +00:00
print_prompt(host, port);
2025-05-25 14:35:35 +00:00
input.clear();
2025-05-25 21:28:01 +00:00
if io::stdin().read_line(&mut input).is_err() {
eprintln!("{}", "Failed to read input".red());
continue;
}
2025-05-25 14:35:35 +00:00
2025-05-25 21:28:01 +00:00
let input = input.trim();
2025-05-25 14:35:35 +00:00
if input.is_empty() {
continue;
}
if input.eq_ignore_ascii_case("quit") || input.eq_ignore_ascii_case("exit") {
break;
}
2025-05-25 20:31:46 +00:00
if !is_valid_command(input) {
2025-05-25 21:28:01 +00:00
eprintln!("{}", "Error: Invalid command format".red());
2025-05-25 20:31:46 +00:00
continue;
}
2025-05-25 14:35:35 +00:00
match send_command(&stream, input) {
2025-05-25 21:28:01 +00:00
Ok(response) => print_response(&response),
2025-05-25 14:35:35 +00:00
Err(e) => {
2025-05-25 21:28:01 +00:00
if is_connection_error(&e) {
eprintln!("{}", "Connection error".red());
2025-05-25 20:31:46 +00:00
break;
}
2025-05-25 21:28:01 +00:00
eprintln!("{}", format!("Error: {}", e.to_string().replace("KeyDB", "Futriix")).red());
2025-05-25 14:35:35 +00:00
}
}
2025-05-24 00:26:45 +03:00
}
2025-05-25 14:35:35 +00:00
}
fn print_prompt(host: &str, port: u16) {
let prompt = format!("{}:{}:{}:~>", PROMPT_NAME, host, port);
print!("{} ", prompt.green());
io::stdout().flush().unwrap();
}
2025-05-25 21:28:01 +00:00
fn is_valid_command(cmd: &str) -> bool {
!cmd.is_empty() &&
!cmd.chars().any(|c| c.is_control()) &&
cmd.split_whitespace().next().is_some()
}
2025-05-25 20:31:46 +00:00
fn send_command(stream: &TcpStream, command: &str) -> io::Result<resp::Value> {
2025-05-25 14:35:35 +00:00
let parts: Vec<&str> = command.split_whitespace().collect();
2025-05-25 21:28:01 +00:00
let mut resp_command = format!("*{}\r\n", parts.len());
2025-05-24 00:26:45 +03:00
2025-05-25 14:35:35 +00:00
for part in parts {
resp_command.push_str(&format!("${}\r\n{}\r\n", part.len(), part));
}
let mut stream = stream.try_clone()?;
stream.write_all(resp_command.as_bytes())?;
2025-05-25 20:31:46 +00:00
let mut decoder = resp::Decoder::new(&stream);
2025-05-25 14:35:35 +00:00
decoder.decode()
2025-05-24 00:26:45 +03:00
}
2025-05-25 14:35:35 +00:00
2025-05-25 20:31:46 +00:00
fn print_response(value: &resp::Value) {
2025-05-25 14:35:35 +00:00
match value {
2025-05-25 20:40:33 +00:00
resp::Value::SimpleString(s) | resp::Value::BulkString(s) => {
2025-05-25 21:28:01 +00:00
println!("{}", s.replace("KeyDB", "Futriix"));
2025-05-25 20:40:33 +00:00
},
resp::Value::Error(e) => {
2025-05-25 21:28:01 +00:00
println!("{}", format!("(error) {}", e.replace("KeyDB", "Futriix")).red());
2025-05-25 20:40:33 +00:00
},
2025-05-25 20:31:46 +00:00
resp::Value::Integer(i) => println!("(integer) {}", i),
resp::Value::Array(arr) => {
2025-05-25 14:35:35 +00:00
for (i, item) in arr.iter().enumerate() {
print!("{}) ", i + 1);
print_response(item);
}
2025-05-25 21:28:01 +00:00
},
2025-05-25 20:31:46 +00:00
resp::Value::Null => println!("(nil)"),
2025-05-25 14:35:35 +00:00
}
}
2025-05-25 21:28:01 +00:00
fn is_connection_error(error: &io::Error) -> bool {
matches!(
error.kind(),
io::ErrorKind::ConnectionAborted |
io::ErrorKind::ConnectionReset |
io::ErrorKind::BrokenPipe
)
2025-05-25 14:35:35 +00:00
}