diff options
| author | salmonik <salmonik@tuta.io> | 2026-10-28 22:36:07 +0100 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-10-28 22:36:07 +0100 |
| commit | 25f9abeb4c69024f8d9dceb472ba33c03e00daa0 (patch) | |
| tree | a02a78f04b5afa2e3fbebffb524f1568df8f00d8 /src/bin | |
| parent | v0.2.4: fix command UX, completion wrapping and auth prompt cancellation (diff) | |
| download | zagan-bet-25f9abeb4c69024f8d9dceb472ba33c03e00daa0.tar.gz zagan-bet-25f9abeb4c69024f8d9dceb472ba33c03e00daa0.zip | |
v0.2.5: fix terminal width, ping timing and output formatting
Diffstat (limited to '')
| -rw-r--r-- | src/bin/client/connection.rs | 14 | ||||
| -rw-r--r-- | src/bin/client/repl.rs | 107 |
2 files changed, 84 insertions, 37 deletions
diff --git a/src/bin/client/connection.rs b/src/bin/client/connection.rs index e54b646..93a7829 100644 --- a/src/bin/client/connection.rs +++ b/src/bin/client/connection.rs | |||
| @@ -1,4 +1,5 @@ | |||
| 1 | use zagan_bet::client::database; | 1 | use zagan_bet::client::database; |
| 2 | use zagan_bet::client::ui::first_contact_lines; | ||
| 2 | use crate::tls::TlsConnector; | 3 | use crate::tls::TlsConnector; |
| 3 | use rusqlite::Connection; | 4 | use rusqlite::Connection; |
| 4 | use std::io::{Read, Write}; | 5 | use std::io::{Read, Write}; |
| @@ -57,10 +58,9 @@ pub fn connect( | |||
| 57 | if let Some(fp) = observed { | 58 | if let Some(fp) = observed { |
| 58 | if database::get_fingerprint(conn, addr).is_none() { | 59 | if database::get_fingerprint(conn, addr).is_none() { |
| 59 | database::save_fingerprint(conn, addr, &fp)?; | 60 | database::save_fingerprint(conn, addr, &fp)?; |
| 60 | println!( | 61 | for line in first_contact_lines(addr, &fp) { |
| 61 | "First contact with {addr}: trusting fingerprint {fp}\n\ | 62 | println!("{}", line); |
| 62 | Future connections must present the same certificate key." | 63 | } |
| 63 | ); | ||
| 64 | } | 64 | } |
| 65 | } | 65 | } |
| 66 | } | 66 | } |
| @@ -72,7 +72,11 @@ pub fn connect( | |||
| 72 | let mut response = String::new(); | 72 | let mut response = String::new(); |
| 73 | tls.read_line(&mut response)?; | 73 | tls.read_line(&mut response)?; |
| 74 | let elapsed = start.elapsed(); | 74 | let elapsed = start.elapsed(); |
| 75 | println!("\n\rConnected to {} with ping delay: {:?} (TLS)", addr, elapsed); | 75 | println!( |
| 76 | "\n\rConnected to {} with ping delay: {:.1}ms (TLS)", | ||
| 77 | addr, | ||
| 78 | elapsed.as_secs_f64() * 1000.0 | ||
| 79 | ); | ||
| 76 | 80 | ||
| 77 | if let Some((username, password)) = database::get_credentials(conn, addr) { | 81 | if let Some((username, password)) = database::get_credentials(conn, addr) { |
| 78 | let auth_msg = format!("AUTH {} {}", username, password); | 82 | let auth_msg = format!("AUTH {} {}", username, password); |
diff --git a/src/bin/client/repl.rs b/src/bin/client/repl.rs index 3fa49b8..76b149b 100644 --- a/src/bin/client/repl.rs +++ b/src/bin/client/repl.rs | |||
| @@ -1,11 +1,13 @@ | |||
| 1 | use rusqlite::Connection; | 1 | use rusqlite::Connection; |
| 2 | use std::io::{stdout, Write}; | 2 | use std::io::{stdout, Write}; |
| 3 | use std::sync::{Arc, Mutex}; | 3 | use std::sync::{Arc, Mutex}; |
| 4 | use std::time::Instant; | ||
| 4 | 5 | ||
| 5 | use crossterm::{ | 6 | use crossterm::{ |
| 6 | cursor::{MoveToColumn, MoveUp, Show}, | 7 | cursor::{MoveToColumn, MoveUp, Show}, |
| 7 | event::{read, Event, KeyCode, KeyEventKind, KeyModifiers}, | 8 | event::{read, Event, KeyCode, KeyEventKind, KeyModifiers}, |
| 8 | execute, | 9 | execute, |
| 10 | style::Stylize, | ||
| 9 | terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType}, | 11 | terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType}, |
| 10 | }; | 12 | }; |
| 11 | 13 | ||
| @@ -13,7 +15,9 @@ use sha2::{Digest, Sha256}; | |||
| 13 | 15 | ||
| 14 | use zagan_bet::client::core::{ClientCore, Effect, InteractiveCommand, Key}; | 16 | use zagan_bet::client::core::{ClientCore, Effect, InteractiveCommand, Key}; |
| 15 | use zagan_bet::client::database; | 17 | use zagan_bet::client::database; |
| 16 | use zagan_bet::client::ui::{input_cursor_column, lines_to_move_up}; | 18 | use zagan_bet::client::ui::{ |
| 19 | display_width, format_pong, host_lines, input_cursor_column, lines_to_move_up, | ||
| 20 | }; | ||
| 17 | 21 | ||
| 18 | use crate::connection::TlsStream; | 22 | use crate::connection::TlsStream; |
| 19 | 23 | ||
| @@ -96,18 +100,22 @@ impl ClientState { | |||
| 96 | 100 | ||
| 97 | loop { | 101 | loop { |
| 98 | let event = read().unwrap(); | 102 | let event = read().unwrap(); |
| 99 | if let Event::Key(key) = event { | 103 | match event { |
| 100 | if key.kind != KeyEventKind::Press { | 104 | Event::Key(key) => { |
| 101 | continue; | 105 | if key.kind != KeyEventKind::Press { |
| 102 | } | 106 | continue; |
| 103 | let Some(key) = map_key(key) else { | 107 | } |
| 104 | continue; | 108 | let Some(key) = map_key(key) else { |
| 105 | }; | 109 | continue; |
| 106 | let effects = self.core.handle_key(key); | 110 | }; |
| 107 | if self.apply_effects(effects) { | 111 | let effects = self.core.handle_key(key); |
| 108 | break; | 112 | if self.apply_effects(effects) { |
| 113 | break; | ||
| 114 | } | ||
| 115 | self.apply_prompt(); | ||
| 109 | } | 116 | } |
| 110 | self.apply_prompt(); | 117 | Event::Resize(_, _) => self.apply_prompt(), |
| 118 | _ => {} | ||
| 111 | } | 119 | } |
| 112 | } | 120 | } |
| 113 | 121 | ||
| @@ -120,15 +128,45 @@ impl ClientState { | |||
| 120 | Effect::Print(lines) => { | 128 | Effect::Print(lines) => { |
| 121 | self.commit_prompt_line(); | 129 | self.commit_prompt_line(); |
| 122 | for line in lines { | 130 | for line in lines { |
| 123 | println!("{}", line); | 131 | let rendered = if line.starts_with("ERROR") |
| 132 | || line.contains("failed") | ||
| 133 | { | ||
| 134 | line.as_str().red().to_string() | ||
| 135 | } else if line.starts_with("Logged in") | ||
| 136 | || line.starts_with("Successfully") | ||
| 137 | { | ||
| 138 | line.as_str().green().to_string() | ||
| 139 | } else if line.starts_with("Available commands:") | ||
| 140 | || line.starts_with("Available games:") | ||
| 141 | || line.starts_with("Remembered hosts:") | ||
| 142 | { | ||
| 143 | line.as_str().bold().cyan().to_string() | ||
| 144 | } else { | ||
| 145 | line | ||
| 146 | }; | ||
| 147 | println!("{}", rendered); | ||
| 124 | } | 148 | } |
| 125 | } | 149 | } |
| 126 | Effect::ReplaceRegion(lines) => self.draw_region(&lines), | 150 | Effect::ReplaceRegion(lines) => self.draw_region(&lines), |
| 127 | Effect::ClearRegion => self.clear_region(), | 151 | Effect::ClearRegion => self.clear_region(), |
| 128 | Effect::SetPrompt { text, cursor_col } => self.draw_prompt(&text, cursor_col), | 152 | Effect::SetPrompt { text, cursor_col } => self.draw_prompt(&text, cursor_col), |
| 129 | Effect::SendLine(line) => { | 153 | Effect::SendLine(line) => { |
| 154 | let is_ping = line == "PING"; | ||
| 155 | let started = Instant::now(); | ||
| 130 | let reply = self.send_and_read(&line); | 156 | let reply = self.send_and_read(&line); |
| 131 | let more = self.core.handle_reply(&reply); | 157 | let elapsed = started.elapsed(); |
| 158 | let mut more = self.core.handle_reply(&reply); | ||
| 159 | if is_ping && reply.trim() == "PONG" { | ||
| 160 | for effect in &mut more { | ||
| 161 | if let Effect::Print(lines) = effect { | ||
| 162 | for text in lines { | ||
| 163 | if text == "Pong" { | ||
| 164 | *text = format_pong(elapsed); | ||
| 165 | } | ||
| 166 | } | ||
| 167 | } | ||
| 168 | } | ||
| 169 | } | ||
| 132 | if self.stream.is_none() { | 170 | if self.stream.is_none() { |
| 133 | self.core.set_connected(false); | 171 | self.core.set_connected(false); |
| 134 | } | 172 | } |
| @@ -196,7 +234,21 @@ impl ClientState { | |||
| 196 | execute!(stdout(), MoveUp(lines_to_move_up(self.game_lines))).unwrap(); | 234 | execute!(stdout(), MoveUp(lines_to_move_up(self.game_lines))).unwrap(); |
| 197 | } | 235 | } |
| 198 | execute!(stdout(), MoveToColumn(0), Clear(ClearType::FromCursorDown)).unwrap(); | 236 | execute!(stdout(), MoveToColumn(0), Clear(ClearType::FromCursorDown)).unwrap(); |
| 199 | print!("{}", lines.join("\r\n")); | 237 | let rendered: Vec<String> = lines |
| 238 | .iter() | ||
| 239 | .map(|line| { | ||
| 240 | if line.starts_with("===") { | ||
| 241 | line.as_str().bold().cyan().to_string() | ||
| 242 | } else if line.contains("JACKPOT") || line.contains("Two of a kind") { | ||
| 243 | line.as_str().bold().green().to_string() | ||
| 244 | } else if line.starts_with("ERROR:") { | ||
| 245 | line.as_str().red().to_string() | ||
| 246 | } else { | ||
| 247 | line.clone() | ||
| 248 | } | ||
| 249 | }) | ||
| 250 | .collect(); | ||
| 251 | print!("{}", rendered.join("\r\n")); | ||
| 200 | stdout().flush().unwrap(); | 252 | stdout().flush().unwrap(); |
| 201 | self.game_lines = lines.len().max(1); | 253 | self.game_lines = lines.len().max(1); |
| 202 | } | 254 | } |
| @@ -217,7 +269,8 @@ impl ClientState { | |||
| 217 | 269 | ||
| 218 | fn draw_prompt(&mut self, text: &str, cursor_col: u16) { | 270 | fn draw_prompt(&mut self, text: &str, cursor_col: u16) { |
| 219 | execute!(stdout(), MoveToColumn(0), Clear(ClearType::CurrentLine)).unwrap(); | 271 | execute!(stdout(), MoveToColumn(0), Clear(ClearType::CurrentLine)).unwrap(); |
| 220 | print!("> {}", text); | 272 | print!("{}", "> ".cyan()); |
| 273 | print!("{}", text); | ||
| 221 | stdout().flush().unwrap(); | 274 | stdout().flush().unwrap(); |
| 222 | execute!(stdout(), MoveToColumn(cursor_col)).unwrap(); | 275 | execute!(stdout(), MoveToColumn(cursor_col)).unwrap(); |
| 223 | self.prompt_text = text.to_string(); | 276 | self.prompt_text = text.to_string(); |
| @@ -376,16 +429,8 @@ impl ClientState { | |||
| 376 | 429 | ||
| 377 | fn list_hosts(&mut self) { | 430 | fn list_hosts(&mut self) { |
| 378 | let hosts = database::get_all_hosts(&self.conn); | 431 | let hosts = database::get_all_hosts(&self.conn); |
| 379 | if hosts.is_empty() { | 432 | for line in host_lines(&hosts) { |
| 380 | println!("No remembered hosts."); | 433 | println!("{}", line); |
| 381 | return; | ||
| 382 | } | ||
| 383 | println!("Remembered hosts:"); | ||
| 384 | for (host, fingerprint) in hosts { | ||
| 385 | match fingerprint { | ||
| 386 | Some(fingerprint) => println!(" {} - {}", host, fingerprint), | ||
| 387 | None => println!(" {} - (no fingerprint)", host), | ||
| 388 | } | ||
| 389 | } | 434 | } |
| 390 | } | 435 | } |
| 391 | } | 436 | } |
| @@ -425,7 +470,6 @@ fn read_line(prompt: &str, cleanup_done: &Arc<Mutex<bool>>) -> String { | |||
| 425 | stdout().flush().unwrap(); | 470 | stdout().flush().unwrap(); |
| 426 | 471 | ||
| 427 | let mut input = String::new(); | 472 | let mut input = String::new(); |
| 428 | let prompt_len = prompt.len(); | ||
| 429 | 473 | ||
| 430 | loop { | 474 | loop { |
| 431 | match read().unwrap() { | 475 | match read().unwrap() { |
| @@ -442,8 +486,8 @@ fn read_line(prompt: &str, cleanup_done: &Arc<Mutex<bool>>) -> String { | |||
| 442 | execute!( | 486 | execute!( |
| 443 | stdout(), | 487 | stdout(), |
| 444 | MoveToColumn(input_cursor_column( | 488 | MoveToColumn(input_cursor_column( |
| 445 | prompt_len, | 489 | display_width(prompt), |
| 446 | input.len() | 490 | display_width(&input) |
| 447 | )), | 491 | )), |
| 448 | Clear(ClearType::UntilNewLine) | 492 | Clear(ClearType::UntilNewLine) |
| 449 | ) | 493 | ) |
| @@ -486,7 +530,6 @@ fn read_password(prompt: &str, cleanup_done: &Arc<Mutex<bool>>) -> String { | |||
| 486 | stdout().flush().unwrap(); | 530 | stdout().flush().unwrap(); |
| 487 | 531 | ||
| 488 | let mut input = String::new(); | 532 | let mut input = String::new(); |
| 489 | let prompt_len = prompt.len(); | ||
| 490 | 533 | ||
| 491 | loop { | 534 | loop { |
| 492 | match read().unwrap() { | 535 | match read().unwrap() { |
| @@ -503,8 +546,8 @@ fn read_password(prompt: &str, cleanup_done: &Arc<Mutex<bool>>) -> String { | |||
| 503 | execute!( | 546 | execute!( |
| 504 | stdout(), | 547 | stdout(), |
| 505 | MoveToColumn(input_cursor_column( | 548 | MoveToColumn(input_cursor_column( |
| 506 | prompt_len, | 549 | display_width(prompt), |
| 507 | input.len() | 550 | display_width(&input) |
| 508 | )), | 551 | )), |
| 509 | Clear(ClearType::UntilNewLine) | 552 | Clear(ClearType::UntilNewLine) |
| 510 | ) | 553 | ) |
