summaryrefslogtreecommitdiffstats
path: root/src/bin/client
diff options
context:
space:
mode:
authorsalmonik <raisersoft@proton.me>2026-02-28 17:35:40 +0100
committersalmonik <raisersoft@proton.me>2026-02-28 17:35:40 +0100
commit28382b249535e964ce983fe5a0821a898a077775 (patch)
tree3b68dd13d9fec27ab3163f09285c718874540c65 /src/bin/client
parentinit version (diff)
downloadzagan-bet-28382b249535e964ce983fe5a0821a898a077775.tar.gz
zagan-bet-28382b249535e964ce983fe5a0821a898a077775.zip
multi file for both sides
Diffstat (limited to 'src/bin/client')
-rw-r--r--src/bin/client/connection.rs32
-rw-r--r--src/bin/client/database.rs30
-rw-r--r--src/bin/client/main.rs17
-rw-r--r--src/bin/client/mod.rs4
-rw-r--r--src/bin/client/platform.rs28
-rw-r--r--src/bin/client/repl.rs108
6 files changed, 219 insertions, 0 deletions
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 @@
1use std::io::{BufRead, BufReader, Write};
2use std::net::TcpStream;
3use std::sync::mpsc;
4use std::thread;
5use std::time::{Duration, Instant};
6
7pub fn connect(addr: &str) -> Result<TcpStream, Box<dyn std::error::Error>> {
8 let (tx, rx) = mpsc::channel();
9 let addr_str = addr.to_string();
10 thread::spawn(move || {
11 match TcpStream::connect(&addr_str) {
12 Ok(s) => tx.send(Ok(s)).unwrap(),
13 Err(e) => tx.send(Err(e)).unwrap(),
14 }
15 });
16 match rx.recv_timeout(Duration::from_secs(3)) {
17 Ok(Ok(s)) => {
18 let mut ping_writer = s.try_clone().unwrap();
19 let mut ping_reader = BufReader::new(s.try_clone().unwrap());
20 let start = Instant::now();
21 writeln!(ping_writer, "PING").unwrap();
22 let mut response = String::new();
23 ping_reader.read_line(&mut response).unwrap();
24 let elapsed = start.elapsed();
25 println!("Connected to {} with ping delay: {:?}", addr, elapsed);
26 Ok(s)
27 }
28 Ok(Err(e)) => Err(Box::new(e)),
29 Err(mpsc::RecvTimeoutError::Timeout) => Err("Connection timed out after 3 seconds".into()),
30 Err(_) => Err("Connection attempt failed".into()),
31 }
32}
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 @@
1use rusqlite::Connection;
2
3pub fn init_db() -> Connection {
4 let conn = Connection::open("client.db").unwrap();
5 conn.execute(
6 "CREATE TABLE IF NOT EXISTS hosts (
7 host TEXT PRIMARY KEY,
8 last_used DATETIME DEFAULT CURRENT_TIMESTAMP
9 )",
10 [],
11 ).unwrap();
12 conn
13}
14
15pub fn get_last_host(conn: &Connection) -> Option<String> {
16 let mut stmt = conn.prepare("SELECT host FROM hosts ORDER BY last_used DESC LIMIT 1").unwrap();
17 let mut rows = stmt.query([]).unwrap();
18 if let Ok(Some(row)) = rows.next() {
19 row.get(0).ok()
20 } else {
21 None
22 }
23}
24
25pub fn save_host(conn: &Connection, host: &str) {
26 conn.execute(
27 "INSERT OR REPLACE INTO hosts (host, last_used) VALUES (?1, CURRENT_TIMESTAMP)",
28 [host],
29 ).unwrap();
30}
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 @@
1mod repl;
2mod database;
3mod connection;
4mod platform;
5
6use repl::ClientState;
7
8fn main() {
9 #[cfg(windows)]
10 platform::win::enable_vt().unwrap();
11
12 let mut state = ClientState::new();
13 if let Some(host) = state.addr.clone() {
14 state.connect_to(&host);
15 }
16 state.run();
17}
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 @@
1pub mod database;
2pub mod connection;
3pub mod platform;
4pub 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 @@
1#[cfg(windows)]
2pub mod win {
3 use std::io;
4 use std::os::windows::io::AsRawHandle;
5 use winapi::um::consoleapi::GetConsoleMode;
6 use winapi::um::consoleapi::SetConsoleMode;
7 use winapi::um::wincon::ENABLE_VIRTUAL_TERMINAL_PROCESSING;
8 use winapi::um::winbase::STD_OUTPUT_HANDLE;
9 use winapi::um::processenv::GetStdHandle;
10 use winapi::shared::minwindef::DWORD;
11
12 pub fn enable_vt() -> io::Result<()> {
13 unsafe {
14 let h = GetStdHandle(STD_OUTPUT_HANDLE);
15 if h.is_null() { return Err(io::Error::last_os_error()); }
16
17 let mut mode: DWORD = 0;
18 if GetConsoleMode(h, &mut mode) == 0 {
19 return Err(io::Error::last_os_error());
20 }
21 let new = mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING;
22 if SetConsoleMode(h, new) == 0 {
23 return Err(io::Error::last_os_error());
24 }
25 Ok(())
26 }
27 }
28}
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 @@
1use std::io::{BufRead, BufReader, Write, stdin, stdout};
2use std::net::TcpStream;
3use rusqlite::Connection;
4
5use crate::connection;
6use crate::database;
7
8pub struct ClientState {
9 pub stream: Option<TcpStream>,
10 pub conn: Connection,
11 pub addr: Option<String>,
12 pub default_port: u16,
13}
14
15impl ClientState {
16 pub fn new() -> Self {
17 let conn = database::init_db();
18 let default_ip = "127.0.0.1".to_string();
19 let default_port = 11525;
20 let last_host = database::get_last_host(&conn);
21 let default_host = format!("{}:{}", default_ip, default_port);
22 let addr = last_host.or(Some(default_host));
23
24 Self {
25 stream: None,
26 conn,
27 addr,
28 default_port,
29 }
30 }
31
32 pub fn connect_to(&mut self, addr: &str) {
33 self.addr = Some(addr.to_string());
34 match connection::connect(addr) {
35 Ok(s) => {
36 self.stream = Some(s);
37 database::save_host(&self.conn, addr);
38 }
39 Err(e) => println!("Failed to connect: {}", e),
40 }
41 }
42
43 pub fn run(&mut self) {
44 loop {
45 print!("> ");
46 stdout().flush().unwrap();
47 let mut line = String::new();
48 stdin().read_line(&mut line).unwrap();
49 let line = line.trim();
50 if line.is_empty() {
51 continue;
52 }
53 let parts: Vec<&str> = line.splitn(2, ' ').collect();
54 match parts[0] {
55 "connect" => {
56 let addr_str = if parts.len() < 2 {
57 if let Some(h) = &self.addr {
58 h.clone()
59 } else {
60 println!("No host specified and no previous connection. Usage: connect <ip>:<port>");
61 continue;
62 }
63 } else {
64 let mut a = parts[1].to_string();
65 if !a.contains(':') {
66 a = format!("{}:{}", a, self.default_port);
67 }
68 a
69 };
70 self.connect_to(&addr_str);
71 }
72 "send" => {
73 if self.stream.is_none() {
74 println!("Not connected. Use 'connect <ip>:<port>' first.");
75 continue;
76 }
77 if parts.len() < 2 {
78 println!("Usage: send <message>");
79 continue;
80 }
81 let msg = parts[1];
82 let s = self.stream.as_mut().unwrap();
83 let writer_stream = s.try_clone().unwrap();
84 let mut writer = writer_stream;
85 let response_reader_stream = s.try_clone().unwrap();
86 let mut response_reader = BufReader::new(response_reader_stream);
87 writeln!(writer, "{}", msg).unwrap();
88 let mut response = String::new();
89 response_reader.read_line(&mut response).unwrap();
90 println!("Echo: {}", response.trim());
91 }
92 "disconnect" => {
93 self.stream = None;
94 println!("Disconnected");
95 }
96 "help" => {
97 help();
98 }
99 "quit" => break,
100 _ => help(),
101 }
102 }
103 }
104}
105
106fn help() {
107 println!("Available commands: connect <ip>:<port>, send <message>, disconnect, help, quit");
108}