diff options
Diffstat (limited to '')
| -rw-r--r-- | src/bin/client/repl.rs | 190 |
1 files changed, 131 insertions, 59 deletions
diff --git a/src/bin/client/repl.rs b/src/bin/client/repl.rs index bc4238d..cfdcdd5 100644 --- a/src/bin/client/repl.rs +++ b/src/bin/client/repl.rs | |||
| @@ -13,12 +13,14 @@ use crossterm::{ | |||
| 13 | 13 | ||
| 14 | use sha2::{Digest, Sha256}; | 14 | use sha2::{Digest, Sha256}; |
| 15 | 15 | ||
| 16 | use zagan_bet::client::core::{ClientCore, Effect, InteractiveCommand}; | 16 | use zagan_bet::client::core::{ClientCore, Effect}; |
| 17 | use zagan_bet::client::input::{game_input, GameInput, Input, Key, Editor}; | 17 | use zagan_bet::client::input::{game_input, Editor, GameInput, Input, Key}; |
| 18 | use zagan_bet::client::shell::{self, Shell}; | ||
| 19 | use zagan_bet::client::view::{RoulettePhase, View}; | ||
| 18 | use zagan_bet::client::database; | 20 | use zagan_bet::client::database; |
| 19 | use zagan_bet::client::ui::{ | 21 | use zagan_bet::client::ui::{ |
| 20 | display_width, format_pong, host_lines, input_cursor_column, lines_to_move_up, | 22 | display_width, format_pong, help_lines, host_lines, input_cursor_column, |
| 21 | terminal_size_warning, visual_line_count, MIN_COLUMNS, MIN_ROWS, | 23 | lines_to_move_up, terminal_size_warning, visual_line_count, MIN_COLUMNS, MIN_ROWS, |
| 22 | }; | 24 | }; |
| 23 | use zagan_bet::protocol::io_failure_message; | 25 | use zagan_bet::protocol::io_failure_message; |
| 24 | use zagan_bet::roulette::{WHEEL, color as roulette_color}; | 26 | use zagan_bet::roulette::{WHEEL, color as roulette_color}; |
| @@ -42,6 +44,8 @@ pub struct ClientState { | |||
| 42 | terminal_size: (u16, u16), | 44 | terminal_size: (u16, u16), |
| 43 | /// Size we already warned about, so a resize does not spam the transcript. | 45 | /// Size we already warned about, so a resize does not spam the transcript. |
| 44 | size_warned: Option<(u16, u16)>, | 46 | size_warned: Option<(u16, u16)>, |
| 47 | /// Last roulette result the wheel animation ran for. | ||
| 48 | last_roulette: Option<u8>, | ||
| 45 | } | 49 | } |
| 46 | 50 | ||
| 47 | impl ClientState { | 51 | impl ClientState { |
| @@ -67,6 +71,7 @@ impl ClientState { | |||
| 67 | prompt_text: String::new(), | 71 | prompt_text: String::new(), |
| 68 | terminal_size: crossterm::terminal::size().unwrap_or((MIN_COLUMNS, MIN_ROWS)), | 72 | terminal_size: crossterm::terminal::size().unwrap_or((MIN_COLUMNS, MIN_ROWS)), |
| 69 | size_warned: None, | 73 | size_warned: None, |
| 74 | last_roulette: None, | ||
| 70 | }) | 75 | }) |
| 71 | } | 76 | } |
| 72 | 77 | ||
| @@ -129,7 +134,7 @@ impl ClientState { | |||
| 129 | } else { | 134 | } else { |
| 130 | match self.editor.handle(key) { | 135 | match self.editor.handle(key) { |
| 131 | Input::Quit => vec![Effect::Quit], | 136 | Input::Quit => vec![Effect::Quit], |
| 132 | Input::Submit(line) => self.core.submit_line(&line), | 137 | Input::Submit(line) => self.submit_line(&line), |
| 133 | Input::Print(lines) => vec![Effect::Print(lines)], | 138 | Input::Print(lines) => vec![Effect::Print(lines)], |
| 134 | Input::Prompt | Input::None => Vec::new(), | 139 | Input::Prompt | Input::None => Vec::new(), |
| 135 | } | 140 | } |
| @@ -233,17 +238,26 @@ impl ClientState { | |||
| 233 | } | 238 | } |
| 234 | } | 239 | } |
| 235 | Effect::Render(view) => { | 240 | Effect::Render(view) => { |
| 241 | if let View::Roulette(roulette) = &view { | ||
| 242 | if roulette.phase == RoulettePhase::Result { | ||
| 243 | if let Some(number) = roulette.number { | ||
| 244 | if self.last_roulette != Some(number) { | ||
| 245 | self.animate_wheel(number); | ||
| 246 | self.last_roulette = Some(number); | ||
| 247 | while crossterm::event::poll(std::time::Duration::ZERO) | ||
| 248 | .unwrap_or(false) | ||
| 249 | { | ||
| 250 | let _ = crossterm::event::read(); | ||
| 251 | } | ||
| 252 | } | ||
| 253 | } | ||
| 254 | } else { | ||
| 255 | self.last_roulette = None; | ||
| 256 | } | ||
| 257 | } | ||
| 236 | let lines = zagan_bet::client::tui::render_view(&view); | 258 | let lines = zagan_bet::client::tui::render_view(&view); |
| 237 | self.draw_region(&lines); | 259 | self.draw_region(&lines); |
| 238 | } | 260 | } |
| 239 | Effect::AnimateRoulette { winning, view } => { | ||
| 240 | self.animate_wheel(winning); | ||
| 241 | let lines = zagan_bet::client::tui::render_view(&view); | ||
| 242 | self.draw_region(&lines); | ||
| 243 | while crossterm::event::poll(std::time::Duration::ZERO).unwrap_or(false) { | ||
| 244 | let _ = crossterm::event::read(); | ||
| 245 | } | ||
| 246 | } | ||
| 247 | Effect::ClearRegion => self.clear_region(), | 261 | Effect::ClearRegion => self.clear_region(), |
| 248 | Effect::SendLine(line) => { | 262 | Effect::SendLine(line) => { |
| 249 | let is_ping = line == "PING"; | 263 | let is_ping = line == "PING"; |
| @@ -269,37 +283,11 @@ impl ClientState { | |||
| 269 | return true; | 283 | return true; |
| 270 | } | 284 | } |
| 271 | } | 285 | } |
| 272 | Effect::Interactive(command) => { | ||
| 273 | self.commit_prompt_line(); | ||
| 274 | self.run_interactive(command); | ||
| 275 | } | ||
| 276 | Effect::Disconnect => { | 286 | Effect::Disconnect => { |
| 277 | self.stream = None; | 287 | self.stream = None; |
| 288 | self.core.reset_after_disconnect(); | ||
| 278 | self.clear_region(); | 289 | self.clear_region(); |
| 279 | } | 290 | } |
| 280 | Effect::ListHosts => { | ||
| 281 | self.commit_prompt_line(); | ||
| 282 | self.list_hosts(); | ||
| 283 | } | ||
| 284 | Effect::ForgetHost(target) => { | ||
| 285 | self.commit_prompt_line(); | ||
| 286 | match database::forget_host(&self.conn, &target) { | ||
| 287 | Ok(outcome) if outcome.credentials_removed => println!( | ||
| 288 | "Cleared trust and stored credentials for {}.", | ||
| 289 | target | ||
| 290 | ), | ||
| 291 | Ok(outcome) => { | ||
| 292 | println!("Cleared trust for {}.", target); | ||
| 293 | if !outcome.siblings.is_empty() { | ||
| 294 | println!( | ||
| 295 | "Stored credentials kept: this server is still reachable as {}.", | ||
| 296 | outcome.siblings.join(", ") | ||
| 297 | ); | ||
| 298 | } | ||
| 299 | } | ||
| 300 | Err(error) => println!("Failed to forget host: {}", error), | ||
| 301 | } | ||
| 302 | } | ||
| 303 | Effect::Quit => { | 291 | Effect::Quit => { |
| 304 | let had_board = self.game_lines > 0; | 292 | let had_board = self.game_lines > 0; |
| 305 | self.clear_region(); | 293 | self.clear_region(); |
| @@ -545,28 +533,112 @@ impl ClientState { | |||
| 545 | } | 533 | } |
| 546 | } | 534 | } |
| 547 | 535 | ||
| 548 | fn run_interactive(&mut self, command: InteractiveCommand) { | 536 | /// Run one command line from the prompt. |
| 549 | match command { | 537 | fn submit_line(&mut self, line: &str) -> Vec<Effect> { |
| 550 | InteractiveCommand::Login => self.do_login(), | 538 | match shell::parse(line) { |
| 551 | InteractiveCommand::Register => self.do_register(), | 539 | Shell::Run(command) => self.core.command(command), |
| 552 | InteractiveCommand::SetCred => self.do_setcred(), | 540 | Shell::Login => { |
| 553 | InteractiveCommand::Connect(target) => { | 541 | self.do_login(); |
| 554 | if self.stream.is_some() { | 542 | Vec::new() |
| 555 | println!("Already connected. Disconnect first."); | 543 | } |
| 544 | Shell::Register => { | ||
| 545 | self.do_register(); | ||
| 546 | Vec::new() | ||
| 547 | } | ||
| 548 | Shell::SetCred => { | ||
| 549 | self.do_setcred(); | ||
| 550 | Vec::new() | ||
| 551 | } | ||
| 552 | Shell::Connect(target) => { | ||
| 553 | self.do_connect(target); | ||
| 554 | Vec::new() | ||
| 555 | } | ||
| 556 | Shell::Disconnect => { | ||
| 557 | self.stream = None; | ||
| 558 | self.core.reset_after_disconnect(); | ||
| 559 | vec![ | ||
| 560 | Effect::ClearRegion, | ||
| 561 | Effect::Print(vec!["Disconnected".to_string()]), | ||
| 562 | ] | ||
| 563 | } | ||
| 564 | Shell::Hosts => { | ||
| 565 | self.list_hosts(); | ||
| 566 | Vec::new() | ||
| 567 | } | ||
| 568 | Shell::ForgetHost(target) => { | ||
| 569 | self.forget_host(target); | ||
| 570 | Vec::new() | ||
| 571 | } | ||
| 572 | Shell::ForgetLogin(target) => { | ||
| 573 | self.forget_login(target); | ||
| 574 | Vec::new() | ||
| 575 | } | ||
| 576 | Shell::Help => vec![Effect::Print(help_lines())], | ||
| 577 | Shell::Quit => vec![Effect::Quit], | ||
| 578 | Shell::Empty => Vec::new(), | ||
| 579 | Shell::Usage(message) => vec![Effect::Print(vec![message])], | ||
| 580 | } | ||
| 581 | } | ||
| 582 | |||
| 583 | fn do_connect(&mut self, target: Option<String>) { | ||
| 584 | if self.stream.is_some() { | ||
| 585 | println!("Already connected. Disconnect first."); | ||
| 586 | return; | ||
| 587 | } | ||
| 588 | let addr = match target { | ||
| 589 | Some(target) => self.normalize_addr(target), | ||
| 590 | None => match &self.addr { | ||
| 591 | Some(addr) => addr.clone(), | ||
| 592 | None => { | ||
| 593 | println!("No host specified. Usage: connect <ip>:<port>"); | ||
| 556 | return; | 594 | return; |
| 557 | } | 595 | } |
| 558 | let addr = match target { | 596 | }, |
| 559 | Some(target) => self.normalize_addr(target), | 597 | }; |
| 560 | None => match &self.addr { | 598 | self.connect_to(&addr); |
| 561 | Some(addr) => addr.clone(), | 599 | } |
| 562 | None => { | 600 | |
| 563 | println!("No host specified. Usage: connect <ip>:<port>"); | 601 | /// Drop the stored login for one server, keeping the pinned key. |
| 564 | return; | 602 | fn forget_login(&mut self, target: Option<String>) { |
| 565 | } | 603 | let Some(host) = target.or_else(|| self.addr.clone()) else { |
| 566 | }, | 604 | println!("Usage: forget-login [ip:port] (or connect first)"); |
| 567 | }; | 605 | return; |
| 568 | self.connect_to(&addr); | 606 | }; |
| 607 | let Some(identity) = database::get_fingerprint(&self.conn, &host) else { | ||
| 608 | println!("No trusted server is remembered as {}.", host); | ||
| 609 | return; | ||
| 610 | }; | ||
| 611 | match database::forget_credentials(&self.conn, &identity) { | ||
| 612 | Ok(true) => println!( | ||
| 613 | "Forgot the saved login for server {} ({}). Trust is unchanged.", | ||
| 614 | short_fingerprint(&identity), | ||
| 615 | host | ||
| 616 | ), | ||
| 617 | Ok(false) => println!("No saved login for server {} ({}).", short_fingerprint(&identity), host), | ||
| 618 | Err(error) => println!("Failed to forget the saved login: {}", error), | ||
| 619 | } | ||
| 620 | } | ||
| 621 | |||
| 622 | fn forget_host(&mut self, target: Option<String>) { | ||
| 623 | let Some(target) = target.or_else(|| self.addr.clone()) else { | ||
| 624 | println!("Usage: forget-host <ip>:<port> (or connect first)"); | ||
| 625 | return; | ||
| 626 | }; | ||
| 627 | match database::forget_host(&self.conn, &target) { | ||
| 628 | Ok(outcome) if outcome.credentials_removed => println!( | ||
| 629 | "Cleared trust and stored credentials for {}.", | ||
| 630 | target | ||
| 631 | ), | ||
| 632 | Ok(outcome) => { | ||
| 633 | println!("Cleared trust for {}.", target); | ||
| 634 | if !outcome.siblings.is_empty() { | ||
| 635 | println!( | ||
| 636 | "Stored credentials kept: this server is still reachable as {}.", | ||
| 637 | outcome.siblings.join(", ") | ||
| 638 | ); | ||
| 639 | } | ||
| 569 | } | 640 | } |
| 641 | Err(error) => println!("Failed to forget host: {}", error), | ||
| 570 | } | 642 | } |
| 571 | } | 643 | } |
| 572 | 644 | ||
