summaryrefslogtreecommitdiffstats
path: root/src/bin
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-11-01 19:19:49 +0100
committersalmonik <salmonik@tuta.io>2026-11-01 19:19:49 +0100
commit24e3bb54db579fb9aad8df2a591527dc77fe4cb4 (patch)
treeaf9a0c290f2675068e8396823f641f24bbe4d308 /src/bin
parentv4.0.6: move the prompt editor and key mapping out of the core (diff)
downloadzagan-bet-24e3bb54db579fb9aad8df2a591527dc77fe4cb4.tar.gz
zagan-bet-24e3bb54db579fb9aad8df2a591527dc77fe4cb4.zip
v4.0.7: finish the Phase 1 boundary with a command shell
The terminal command line moves to client::shell: a typed line becomes a core Command or an adapter action (connect, login, register, set-credential, host maintenance). ClientCore loses submit_line and the last TUI-shaped effects (Interactive, SetPrompt, ListHosts, ForgetHost, AnimateRoulette); the adapter animates roulette from the Result view. Adds forget-login, which drops the stored credential for the pinned key and keeps trust, distinct from logout (session only) and forget-host (trust too).
Diffstat (limited to '')
-rw-r--r--src/bin/client/repl.rs190
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
14use sha2::{Digest, Sha256}; 14use sha2::{Digest, Sha256};
15 15
16use zagan_bet::client::core::{ClientCore, Effect, InteractiveCommand}; 16use zagan_bet::client::core::{ClientCore, Effect};
17use zagan_bet::client::input::{game_input, GameInput, Input, Key, Editor}; 17use zagan_bet::client::input::{game_input, Editor, GameInput, Input, Key};
18use zagan_bet::client::shell::{self, Shell};
19use zagan_bet::client::view::{RoulettePhase, View};
18use zagan_bet::client::database; 20use zagan_bet::client::database;
19use zagan_bet::client::ui::{ 21use 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};
23use zagan_bet::protocol::io_failure_message; 25use zagan_bet::protocol::io_failure_message;
24use zagan_bet::roulette::{WHEEL, color as roulette_color}; 26use 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
47impl ClientState { 51impl 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