From 28382b249535e964ce983fe5a0821a898a077775 Mon Sep 17 00:00:00 2001 From: salmonik Date: Sat, 28 Feb 2026 17:35:40 +0100 Subject: multi file for both sides --- src/bin/client/connection.rs | 32 +++++++++++++ src/bin/client/database.rs | 30 ++++++++++++ src/bin/client/main.rs | 17 +++++++ src/bin/client/mod.rs | 4 ++ src/bin/client/platform.rs | 28 +++++++++++ src/bin/client/repl.rs | 108 +++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 219 insertions(+) create mode 100644 src/bin/client/connection.rs create mode 100644 src/bin/client/database.rs create mode 100644 src/bin/client/main.rs create mode 100644 src/bin/client/mod.rs create mode 100644 src/bin/client/platform.rs create mode 100644 src/bin/client/repl.rs (limited to 'src/bin/client') diff --git a/src/bin/client/connection.rs b/src/bin/client/connection.rs new file mode 100644 index 0000000..9e402cb --- /dev/null +++ b/src/bin/client/connection.rs @@ -0,0 +1,32 @@ +use std::io::{BufRead, BufReader, Write}; +use std::net::TcpStream; +use std::sync::mpsc; +use std::thread; +use std::time::{Duration, Instant}; + +pub fn connect(addr: &str) -> Result> { + let (tx, rx) = mpsc::channel(); + let addr_str = addr.to_string(); + thread::spawn(move || { + match TcpStream::connect(&addr_str) { + Ok(s) => tx.send(Ok(s)).unwrap(), + Err(e) => tx.send(Err(e)).unwrap(), + } + }); + match rx.recv_timeout(Duration::from_secs(3)) { + Ok(Ok(s)) => { + let mut ping_writer = s.try_clone().unwrap(); + let mut ping_reader = BufReader::new(s.try_clone().unwrap()); + let start = Instant::now(); + writeln!(ping_writer, "PING").unwrap(); + let mut response = String::new(); + ping_reader.read_line(&mut response).unwrap(); + let elapsed = start.elapsed(); + println!("Connected to {} with ping delay: {:?}", addr, elapsed); + Ok(s) + } + Ok(Err(e)) => Err(Box::new(e)), + Err(mpsc::RecvTimeoutError::Timeout) => Err("Connection timed out after 3 seconds".into()), + Err(_) => Err("Connection attempt failed".into()), + } +} diff --git a/src/bin/client/database.rs b/src/bin/client/database.rs new file mode 100644 index 0000000..bc8f6f8 --- /dev/null +++ b/src/bin/client/database.rs @@ -0,0 +1,30 @@ +use rusqlite::Connection; + +pub fn init_db() -> Connection { + let conn = Connection::open("client.db").unwrap(); + conn.execute( + "CREATE TABLE IF NOT EXISTS hosts ( + host TEXT PRIMARY KEY, + last_used DATETIME DEFAULT CURRENT_TIMESTAMP + )", + [], + ).unwrap(); + conn +} + +pub fn get_last_host(conn: &Connection) -> Option { + let mut stmt = conn.prepare("SELECT host FROM hosts ORDER BY last_used DESC LIMIT 1").unwrap(); + let mut rows = stmt.query([]).unwrap(); + if let Ok(Some(row)) = rows.next() { + row.get(0).ok() + } else { + None + } +} + +pub fn save_host(conn: &Connection, host: &str) { + conn.execute( + "INSERT OR REPLACE INTO hosts (host, last_used) VALUES (?1, CURRENT_TIMESTAMP)", + [host], + ).unwrap(); +} diff --git a/src/bin/client/main.rs b/src/bin/client/main.rs new file mode 100644 index 0000000..ce9c92f --- /dev/null +++ b/src/bin/client/main.rs @@ -0,0 +1,17 @@ +mod repl; +mod database; +mod connection; +mod platform; + +use repl::ClientState; + +fn main() { + #[cfg(windows)] + platform::win::enable_vt().unwrap(); + + let mut state = ClientState::new(); + if let Some(host) = state.addr.clone() { + state.connect_to(&host); + } + state.run(); +} diff --git a/src/bin/client/mod.rs b/src/bin/client/mod.rs new file mode 100644 index 0000000..938d862 --- /dev/null +++ b/src/bin/client/mod.rs @@ -0,0 +1,4 @@ +pub mod database; +pub mod connection; +pub mod platform; +pub mod repl; diff --git a/src/bin/client/platform.rs b/src/bin/client/platform.rs new file mode 100644 index 0000000..1a6d71b --- /dev/null +++ b/src/bin/client/platform.rs @@ -0,0 +1,28 @@ +#[cfg(windows)] +pub mod win { + use std::io; + use std::os::windows::io::AsRawHandle; + use winapi::um::consoleapi::GetConsoleMode; + use winapi::um::consoleapi::SetConsoleMode; + use winapi::um::wincon::ENABLE_VIRTUAL_TERMINAL_PROCESSING; + use winapi::um::winbase::STD_OUTPUT_HANDLE; + use winapi::um::processenv::GetStdHandle; + use winapi::shared::minwindef::DWORD; + + pub fn enable_vt() -> io::Result<()> { + unsafe { + let h = GetStdHandle(STD_OUTPUT_HANDLE); + if h.is_null() { return Err(io::Error::last_os_error()); } + + let mut mode: DWORD = 0; + if GetConsoleMode(h, &mut mode) == 0 { + return Err(io::Error::last_os_error()); + } + let new = mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING; + if SetConsoleMode(h, new) == 0 { + return Err(io::Error::last_os_error()); + } + Ok(()) + } + } +} diff --git a/src/bin/client/repl.rs b/src/bin/client/repl.rs new file mode 100644 index 0000000..d38c54d --- /dev/null +++ b/src/bin/client/repl.rs @@ -0,0 +1,108 @@ +use std::io::{BufRead, BufReader, Write, stdin, stdout}; +use std::net::TcpStream; +use rusqlite::Connection; + +use crate::connection; +use crate::database; + +pub struct ClientState { + pub stream: Option, + pub conn: Connection, + pub addr: Option, + pub default_port: u16, +} + +impl ClientState { + pub fn new() -> Self { + let conn = database::init_db(); + let default_ip = "127.0.0.1".to_string(); + let default_port = 11525; + let last_host = database::get_last_host(&conn); + let default_host = format!("{}:{}", default_ip, default_port); + let addr = last_host.or(Some(default_host)); + + Self { + stream: None, + conn, + addr, + default_port, + } + } + + pub fn connect_to(&mut self, addr: &str) { + self.addr = Some(addr.to_string()); + match connection::connect(addr) { + Ok(s) => { + self.stream = Some(s); + database::save_host(&self.conn, addr); + } + Err(e) => println!("Failed to connect: {}", e), + } + } + + pub fn run(&mut self) { + loop { + print!("> "); + stdout().flush().unwrap(); + let mut line = String::new(); + stdin().read_line(&mut line).unwrap(); + let line = line.trim(); + if line.is_empty() { + continue; + } + let parts: Vec<&str> = line.splitn(2, ' ').collect(); + match parts[0] { + "connect" => { + let addr_str = if parts.len() < 2 { + if let Some(h) = &self.addr { + h.clone() + } else { + println!("No host specified and no previous connection. Usage: connect :"); + continue; + } + } else { + let mut a = parts[1].to_string(); + if !a.contains(':') { + a = format!("{}:{}", a, self.default_port); + } + a + }; + self.connect_to(&addr_str); + } + "send" => { + if self.stream.is_none() { + println!("Not connected. Use 'connect :' first."); + continue; + } + if parts.len() < 2 { + println!("Usage: send "); + continue; + } + let msg = parts[1]; + let s = self.stream.as_mut().unwrap(); + let writer_stream = s.try_clone().unwrap(); + let mut writer = writer_stream; + let response_reader_stream = s.try_clone().unwrap(); + let mut response_reader = BufReader::new(response_reader_stream); + writeln!(writer, "{}", msg).unwrap(); + let mut response = String::new(); + response_reader.read_line(&mut response).unwrap(); + println!("Echo: {}", response.trim()); + } + "disconnect" => { + self.stream = None; + println!("Disconnected"); + } + "help" => { + help(); + } + "quit" => break, + _ => help(), + } + } + } +} + +fn help() { + println!("Available commands: connect :, send , disconnect, help, quit"); +} -- cgit v1.2.3