From 9ed5958b1057dce8bfcb34a9a983a86d0e1a720c Mon Sep 17 00:00:00 2001 From: salmonik Date: Sat, 28 Feb 2026 18:16:50 +0100 Subject: raw mode for client --- src/bin/client/connection.rs | 2 +- src/bin/client/repl.rs | 215 ++++++++++++++++++++++++++++++++----------- 2 files changed, 161 insertions(+), 56 deletions(-) (limited to 'src/bin') diff --git a/src/bin/client/connection.rs b/src/bin/client/connection.rs index 9e402cb..d03dc1a 100644 --- a/src/bin/client/connection.rs +++ b/src/bin/client/connection.rs @@ -22,7 +22,7 @@ pub fn connect(addr: &str) -> Result> { let mut response = String::new(); ping_reader.read_line(&mut response).unwrap(); let elapsed = start.elapsed(); - println!("Connected to {} with ping delay: {:?}", addr, elapsed); + println!("\n\rConnected to {} with ping delay: {:?}", addr, elapsed); Ok(s) } Ok(Err(e)) => Err(Box::new(e)), diff --git a/src/bin/client/repl.rs b/src/bin/client/repl.rs index d38c54d..1512091 100644 --- a/src/bin/client/repl.rs +++ b/src/bin/client/repl.rs @@ -1,15 +1,29 @@ -use std::io::{BufRead, BufReader, Write, stdin, stdout}; +use std::io::{BufRead, BufReader, Write, stdout}; use std::net::TcpStream; use rusqlite::Connection; +use std::time::Instant; use crate::connection; use crate::database; +use crossterm::{event::{read, Event, KeyCode, KeyModifiers}, terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType}, cursor::{MoveToColumn, Show}, execute}; + +macro_rules! println { + () => { + print!("\r\n") + }; + ($($arg:tt)*) => { + print!("\r{}\n", format_args!($($arg)*)) + }; +} + pub struct ClientState { pub stream: Option, pub conn: Connection, pub addr: Option, pub default_port: u16, + pub history: Vec, + pub history_index: usize, } impl ClientState { @@ -26,6 +40,8 @@ impl ClientState { conn, addr, default_port, + history: vec![], + history_index: 0, } } @@ -41,68 +57,157 @@ impl ClientState { } 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; + enable_raw_mode().unwrap(); + 'outer: loop { + let mut current_input = String::new(); + self.history_index = self.history.len(); + redraw(""); + loop { + match read().unwrap() { + Event::Key(key) => match key.code { + KeyCode::Up => { + if self.history_index > 0 { + self.history_index -= 1; + current_input = self.history[self.history_index].clone(); + redraw(¤t_input); + } } - } else { - let mut a = parts[1].to_string(); - if !a.contains(':') { - a = format!("{}:{}", a, self.default_port); + KeyCode::Down => { + if self.history_index < self.history.len() { + self.history_index += 1; + current_input = if self.history_index == self.history.len() { "".to_string() } else { self.history[self.history_index].clone() }; + redraw(¤t_input); + } } - 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; + KeyCode::Enter => { + let line = current_input.trim().to_string(); + if !line.is_empty() { + self.history.push(line.clone()); + } + // Process command + 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!("\nNo host specified and no previous connection. Usage: connect :"); + continue 'outer; + } + } 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!("\nNot connected. Use 'connect :' first."); + continue 'outer; + } + if parts.len() < 2 { + println!("\nUsage: send "); + continue 'outer; + } + 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); + if let Err(_) = writeln!(writer, "{}", msg) { + self.stream = None; + println!("\nConnection lost"); + continue 'outer; + } + let mut response = String::new(); + if let Err(_) = response_reader.read_line(&mut response) { + self.stream = None; + println!("\nConnection lost"); + } else { + println!("\nEcho: {}", response.trim()); + } + } + "disconnect" => { + self.stream = None; + println!("\nDisconnected"); + } + "ping" => { + if self.stream.is_none() { + println!("\nNot connected. Use 'connect :' first."); + continue 'outer; + } + let s = self.stream.as_mut().unwrap(); + let writer_stream = s.try_clone().unwrap(); + let mut writer = writer_stream; + let start = Instant::now(); + let response_reader_stream = s.try_clone().unwrap(); + let mut response_reader = BufReader::new(response_reader_stream); + if let Err(_) = writeln!(writer, "PING") { + self.stream = None; + println!("\nConnection lost"); + continue 'outer; + } + let mut response = String::new(); + if let Err(_) = response_reader.read_line(&mut response) { + self.stream = None; + println!("\nConnection lost"); + } else { + if response.trim() == "PING" { + let elapsed = start.elapsed(); + println!("\nPong ({:?})", elapsed); + } else { + self.stream = None; + println!("\nConnection lost"); + } + } + } + "help" => { + help(); + } + "quit" => break 'outer, + _ => help(), + } + break; + } + KeyCode::Backspace => { + if !current_input.is_empty() { + current_input.pop(); + redraw(¤t_input); + } + } + KeyCode::Char(c) => { + if key.modifiers.contains(KeyModifiers::CONTROL) && c == 'c' { + disable_raw_mode().unwrap(); + execute!(stdout(), Show).unwrap(); + return; + } else { + current_input.push(c); + redraw(¤t_input); + } + } + _ => {} } - 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(), } } + disable_raw_mode().unwrap(); + execute!(stdout(), Show).unwrap(); } } +fn redraw(current_input: &str) { + execute!(stdout(), Clear(ClearType::CurrentLine), MoveToColumn(0)).unwrap(); + print!("> {}", current_input); + stdout().flush().unwrap(); + execute!(stdout(), MoveToColumn((2 + current_input.len()) as u16)).unwrap(); +} + fn help() { - println!("Available commands: connect :, send , disconnect, help, quit"); + println!("Available commands: connect :, send , disconnect, ping, help, quit"); } -- cgit v1.2.3