diff options
| author | salmonik <salmonik@tuta.io> | 2026-10-29 23:06:22 +0100 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-10-29 23:06:22 +0100 |
| commit | 810e212b4b255914cfa73e85b52ec23db281c0e3 (patch) | |
| tree | 01e8035ce2945112af5ef41ef8959890bb0ef722 /src | |
| parent | v3.0.0: cursor-driven roulette felt with splits and corners; drop schema migr... (diff) | |
| download | zagan-bet-810e212b4b255914cfa73e85b52ec23db281c0e3.tar.gz zagan-bet-810e212b4b255914cfa73e85b52ec23db281c0e3.zip | |
v3.1.0: warn on shrink, but let a live round finish
Diffstat (limited to 'src')
| -rw-r--r-- | src/bin/client/repl.rs | 59 | ||||
| -rw-r--r-- | src/client/core.rs | 22 | ||||
| -rw-r--r-- | src/client/ui.rs | 10 |
3 files changed, 89 insertions, 2 deletions
diff --git a/src/bin/client/repl.rs b/src/bin/client/repl.rs index 2cf121d..bf02c8d 100644 --- a/src/bin/client/repl.rs +++ b/src/bin/client/repl.rs | |||
| @@ -17,7 +17,7 @@ use zagan_bet::client::core::{ClientCore, Effect, InteractiveCommand, Key}; | |||
| 17 | use zagan_bet::client::database; | 17 | use zagan_bet::client::database; |
| 18 | use zagan_bet::client::ui::{ | 18 | use zagan_bet::client::ui::{ |
| 19 | display_width, format_pong, host_lines, input_cursor_column, lines_to_move_up, | 19 | display_width, format_pong, host_lines, input_cursor_column, lines_to_move_up, |
| 20 | visual_line_count, | 20 | terminal_size_warning, visual_line_count, MIN_COLUMNS, MIN_ROWS, |
| 21 | }; | 21 | }; |
| 22 | use zagan_bet::protocol::io_failure_message; | 22 | use zagan_bet::protocol::io_failure_message; |
| 23 | use zagan_bet::roulette::{WHEEL, color as roulette_color}; | 23 | use zagan_bet::roulette::{WHEEL, color as roulette_color}; |
| @@ -36,6 +36,10 @@ pub struct ClientState { | |||
| 36 | /// Text last drawn after the `> ` prompt, used to keep the command echo | 36 | /// Text last drawn after the `> ` prompt, used to keep the command echo |
| 37 | /// in the transcript before printing command output. | 37 | /// in the transcript before printing command output. |
| 38 | prompt_text: String, | 38 | prompt_text: String, |
| 39 | /// Last known terminal size, updated from resize events. | ||
| 40 | terminal_size: (u16, u16), | ||
| 41 | /// Size we already warned about, so a resize does not spam the transcript. | ||
| 42 | size_warned: Option<(u16, u16)>, | ||
| 39 | } | 43 | } |
| 40 | 44 | ||
| 41 | impl ClientState { | 45 | impl ClientState { |
| @@ -60,6 +64,8 @@ impl ClientState { | |||
| 60 | cleanup_done: Arc::new(Mutex::new(false)), | 64 | cleanup_done: Arc::new(Mutex::new(false)), |
| 61 | game_lines: 0, | 65 | game_lines: 0, |
| 62 | prompt_text: String::new(), | 66 | prompt_text: String::new(), |
| 67 | terminal_size: crossterm::terminal::size().unwrap_or((MIN_COLUMNS, MIN_ROWS)), | ||
| 68 | size_warned: None, | ||
| 63 | } | 69 | } |
| 64 | } | 70 | } |
| 65 | 71 | ||
| @@ -114,7 +120,15 @@ impl ClientState { | |||
| 114 | } | 120 | } |
| 115 | } | 121 | } |
| 116 | } | 122 | } |
| 117 | Event::Resize(_, _) => self.apply_prompt(), | 123 | Event::Resize(columns, rows) => { |
| 124 | self.terminal_size = (columns, rows); | ||
| 125 | if self.core.is_in_game() { | ||
| 126 | let effects = self.core.refresh_board(); | ||
| 127 | self.apply_effects(effects); | ||
| 128 | } else { | ||
| 129 | self.apply_prompt(); | ||
| 130 | } | ||
| 131 | } | ||
| 118 | _ => {} | 132 | _ => {} |
| 119 | } | 133 | } |
| 120 | } | 134 | } |
| @@ -130,6 +144,10 @@ impl ClientState { | |||
| 130 | } | 144 | } |
| 131 | } | 145 | } |
| 132 | 146 | ||
| 147 | if !quit { | ||
| 148 | quit = self.warn_about_size(); | ||
| 149 | } | ||
| 150 | |||
| 133 | if quit { | 151 | if quit { |
| 134 | break; | 152 | break; |
| 135 | } | 153 | } |
| @@ -292,7 +310,42 @@ impl ClientState { | |||
| 292 | self.prompt_text.clear(); | 310 | self.prompt_text.clear(); |
| 293 | } | 311 | } |
| 294 | 312 | ||
| 313 | /// Warn once per size when the terminal is too small and no round is live. | ||
| 314 | /// Inside a game the warning is appended to the board instead, and during a | ||
| 315 | /// live round it is suppressed until the round settles. | ||
| 316 | fn warn_about_size(&mut self) -> bool { | ||
| 317 | let size = self.terminal_size; | ||
| 318 | let Some(message) = terminal_size_warning(size.0, size.1) else { | ||
| 319 | self.size_warned = None; | ||
| 320 | return false; | ||
| 321 | }; | ||
| 322 | if self.core.is_in_game() || self.size_warned == Some(size) { | ||
| 323 | return false; | ||
| 324 | } | ||
| 325 | self.size_warned = Some(size); | ||
| 326 | self.apply_effects(vec![Effect::Print(vec![message])]); | ||
| 327 | false | ||
| 328 | } | ||
| 329 | |||
| 295 | fn draw_region(&mut self, lines: &[String]) { | 330 | fn draw_region(&mut self, lines: &[String]) { |
| 331 | let warning = if self.core.is_live_game() { | ||
| 332 | None | ||
| 333 | } else { | ||
| 334 | terminal_size_warning(self.terminal_size.0, self.terminal_size.1) | ||
| 335 | }; | ||
| 336 | let owned; | ||
| 337 | let lines: &[String] = match warning { | ||
| 338 | Some(warning) => { | ||
| 339 | owned = { | ||
| 340 | let mut lines = lines.to_vec(); | ||
| 341 | lines.push(warning); | ||
| 342 | lines | ||
| 343 | }; | ||
| 344 | &owned | ||
| 345 | } | ||
| 346 | None => lines, | ||
| 347 | }; | ||
| 348 | |||
| 296 | if self.game_lines == 0 { | 349 | if self.game_lines == 0 { |
| 297 | self.commit_prompt_line(); | 350 | self.commit_prompt_line(); |
| 298 | } else { | 351 | } else { |
| @@ -308,6 +361,8 @@ impl ClientState { | |||
| 308 | .map(|line| { | 361 | .map(|line| { |
| 309 | if line.starts_with("===") { | 362 | if line.starts_with("===") { |
| 310 | line.as_str().bold().cyan().to_string() | 363 | line.as_str().bold().cyan().to_string() |
| 364 | } else if line.starts_with("WARNING:") { | ||
| 365 | line.as_str().bold().red().to_string() | ||
| 311 | } else if roulette { | 366 | } else if roulette { |
| 312 | if line.starts_with("Won ") { | 367 | if line.starts_with("Won ") { |
| 313 | line.as_str().bold().green().to_string() | 368 | line.as_str().bold().green().to_string() |
diff --git a/src/client/core.rs b/src/client/core.rs index 8411d47..95fa050 100644 --- a/src/client/core.rs +++ b/src/client/core.rs | |||
| @@ -138,6 +138,28 @@ impl ClientCore { | |||
| 138 | self.game.is_in_game() | 138 | self.game.is_in_game() |
| 139 | } | 139 | } |
| 140 | 140 | ||
| 141 | /// True while a round is running and leaving would forfeit a reserved bet. | ||
| 142 | /// The terminal may shrink during one of these without interrupting it. | ||
| 143 | pub fn is_live_game(&self) -> bool { | ||
| 144 | match self | ||
| 145 | .game | ||
| 146 | .get_current_state() | ||
| 147 | .map(|state| state.game_type.as_str()) | ||
| 148 | { | ||
| 149 | Some("blackjack") => self | ||
| 150 | .game | ||
| 151 | .blackjack_state() | ||
| 152 | .is_some_and(|hand| hand.phase == "player"), | ||
| 153 | Some("crash") => self.crash_started.is_some(), | ||
| 154 | _ => false, | ||
| 155 | } | ||
| 156 | } | ||
| 157 | |||
| 158 | /// Redraw the current board, for terminal resizes. | ||
| 159 | pub fn refresh_board(&self) -> Vec<Effect> { | ||
| 160 | self.board_effects() | ||
| 161 | } | ||
| 162 | |||
| 141 | pub fn prompt_effect(&self) -> Option<Effect> { | 163 | pub fn prompt_effect(&self) -> Option<Effect> { |
| 142 | if self.is_in_game() { | 164 | if self.is_in_game() { |
| 143 | return None; | 165 | return None; |
diff --git a/src/client/ui.rs b/src/client/ui.rs index 6161166..ca001c2 100644 --- a/src/client/ui.rs +++ b/src/client/ui.rs | |||
| @@ -261,6 +261,16 @@ pub fn terminal_too_small(columns: u16, rows: u16) -> Option<String> { | |||
| 261 | )) | 261 | )) |
| 262 | } | 262 | } |
| 263 | 263 | ||
| 264 | /// One-line warning for a terminal that shrank while the client is running. | ||
| 265 | pub fn terminal_size_warning(columns: u16, rows: u16) -> Option<String> { | ||
| 266 | if columns >= MIN_COLUMNS && rows >= MIN_ROWS { | ||
| 267 | return None; | ||
| 268 | } | ||
| 269 | Some(format!( | ||
| 270 | "WARNING: terminal is {columns}x{rows}; zagan.bet needs at least {MIN_COLUMNS}x{MIN_ROWS}. Resize to fit." | ||
| 271 | )) | ||
| 272 | } | ||
| 273 | |||
| 264 | /// Render remembered hosts. | 274 | /// Render remembered hosts. |
| 265 | /// | 275 | /// |
| 266 | /// Two addresses that present the same certificate key are the same server, so | 276 | /// Two addresses that present the same certificate key are the same server, so |
