From 25f9abeb4c69024f8d9dceb472ba33c03e00daa0 Mon Sep 17 00:00:00 2001 From: salmonik Date: Wed, 28 Oct 2026 22:36:07 +0100 Subject: v0.2.5: fix terminal width, ping timing and output formatting --- src/client/core.rs | 6 ++-- src/client/ui.rs | 93 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 3 deletions(-) (limited to 'src/client') diff --git a/src/client/core.rs b/src/client/core.rs index a910a68..c204cb2 100644 --- a/src/client/core.rs +++ b/src/client/core.rs @@ -1,8 +1,8 @@ use crate::client::games::manager::GameManager; use crate::client::games::response::parse_spin_response; use crate::client::ui::{ - command_names, completion_lines, help_lines, input_cursor_column, parse_bet_arg, - record_history, + command_names, completion_lines, display_width, help_lines, input_cursor_column, + parse_bet_arg, record_history, }; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -120,7 +120,7 @@ impl ClientCore { } Some(Effect::SetPrompt { text: self.input.clone(), - cursor_col: input_cursor_column(2, self.input.chars().count()), + cursor_col: input_cursor_column(2, display_width(&self.input)), }) } diff --git a/src/client/ui.rs b/src/client/ui.rs index 6fc2f62..493f660 100644 --- a/src/client/ui.rs +++ b/src/client/ui.rs @@ -153,6 +153,94 @@ pub fn parse_bet_arg(raw: &str) -> Result { Ok(bet) } +/// Number of terminal columns used by `text`. +/// +/// This is a deliberately small approximation of `unicode-width`: it handles +/// ASCII, Latin diacritics, combining marks, CJK, and the common emoji ranges +/// without adding a dependency. +pub fn display_width(text: &str) -> usize { + text.chars().map(char_width).sum() +} + +fn char_width(c: char) -> usize { + let cp = c as u32; + if cp < 0x20 || cp == 0x7F { + return 0; + } + if is_zero_width(cp) || is_combining(cp) { + return 0; + } + if is_wide(cp) { + 2 + } else { + 1 + } +} + +fn is_zero_width(cp: u32) -> bool { + matches!( + cp, + 0x200B..=0x200F | 0xFE00..=0xFE0F | 0xFEFF | 0x1F3FB..=0x1F3FF + ) +} + +fn is_combining(cp: u32) -> bool { + matches!( + cp, + 0x0300..=0x036F + | 0x1AB0..=0x1AFF + | 0x1DC0..=0x1DFF + | 0x20D0..=0x20FF + | 0xFE20..=0xFE2F + ) +} + +fn is_wide(cp: u32) -> bool { + matches!( + cp, + 0x1100..=0x115F + | 0x2329..=0x232A + | 0x2E80..=0xA4CF + | 0xAC00..=0xD7A3 + | 0xF900..=0xFAFF + | 0xFE10..=0xFE19 + | 0xFE30..=0xFE6F + | 0xFF00..=0xFF60 + | 0xFFE0..=0xFFE6 + | 0x1F300..=0x1F64F + | 0x1F680..=0x1F6FF + | 0x1F900..=0x1F9FF + | 0x20000..=0x3FFFD + ) +} + +/// The three lines printed on first contact with a host. +pub fn first_contact_lines(addr: &str, fingerprint: &str) -> Vec { + vec![ + format!("First contact with {addr}: trusting a new certificate."), + format!("Fingerprint: {fingerprint}"), + "Future connections must present the same certificate key.".to_string(), + ] +} + +/// Format remembered hosts so a 64-character fingerprint does not wrap on an +/// 80-column terminal. +pub fn host_lines(hosts: &[(String, Option)]) -> Vec { + if hosts.is_empty() { + return vec!["No remembered hosts.".to_string()]; + } + + let mut lines = vec!["Remembered hosts:".to_string()]; + for (host, fingerprint) in hosts { + lines.push(format!(" {host}")); + match fingerprint { + Some(fingerprint) => lines.push(format!(" fingerprint: {fingerprint}")), + None => lines.push(" (no fingerprint)".to_string()), + } + } + lines +} + /// Lines to move the cursor up to reach the first line of a region that ends /// on the current line. `render()` has no trailing newline, so a 7-line board /// needs `MoveUp(6)`. @@ -168,3 +256,8 @@ pub fn input_cursor_column(prompt_len: usize, input_len: usize) -> u16 { .saturating_add(input_len) .min(u16::MAX as usize) as u16 } + +/// Format a ping round-trip time with one decimal place. +pub fn format_pong(elapsed: std::time::Duration) -> String { + format!("Pong ({:.1}ms)", elapsed.as_secs_f64() * 1000.0) +} -- cgit v1.2.3