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 --- .gitignore | 1 + Cargo.lock | 225 +++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + client.db | Bin 12288 -> 0 bytes src/bin/client/connection.rs | 2 +- src/bin/client/repl.rs | 215 ++++++++++++++++++++++++++++++----------- 6 files changed, 388 insertions(+), 56 deletions(-) delete mode 100644 client.db diff --git a/.gitignore b/.gitignore index ea8c4bf..92d1bc1 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +client.db \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 63805f6..dc9de2d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,6 +32,41 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "crossterm" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +dependencies = [ + "bitflags", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "fallible-iterator" version = "0.2.0" @@ -63,6 +98,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + [[package]] name = "libsqlite3-sys" version = "0.26.0" @@ -73,12 +114,62 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + [[package]] name = "pkg-config" version = "0.3.32" @@ -103,6 +194,15 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + [[package]] name = "rusqlite" version = "0.29.0" @@ -117,6 +217,43 @@ dependencies = [ "smallvec", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + [[package]] name = "smallvec" version = "1.15.1" @@ -152,6 +289,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + [[package]] name = "winapi" version = "0.3.9" @@ -174,10 +317,92 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "zagan-bet" version = "0.1.0" dependencies = [ + "crossterm", "rusqlite", "winapi", ] diff --git a/Cargo.toml b/Cargo.toml index 85cacac..1efb533 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,3 +6,4 @@ edition = "2024" [dependencies] rusqlite = "0.29" winapi = { version = "0.3", features = ["consoleapi", "processenv", "wincon", "minwindef", "winbase"] } +crossterm = "0.27" diff --git a/client.db b/client.db deleted file mode 100644 index 8ead0e2..0000000 Binary files a/client.db and /dev/null differ 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