diff options
Diffstat (limited to '')
| -rw-r--r-- | src/client/ui.rs | 93 |
1 files changed, 93 insertions, 0 deletions
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<u64, String> { | |||
| 153 | Ok(bet) | 153 | Ok(bet) |
| 154 | } | 154 | } |
| 155 | 155 | ||
| 156 | /// Number of terminal columns used by `text`. | ||
| 157 | /// | ||
| 158 | /// This is a deliberately small approximation of `unicode-width`: it handles | ||
| 159 | /// ASCII, Latin diacritics, combining marks, CJK, and the common emoji ranges | ||
| 160 | /// without adding a dependency. | ||
| 161 | pub fn display_width(text: &str) -> usize { | ||
| 162 | text.chars().map(char_width).sum() | ||
| 163 | } | ||
| 164 | |||
| 165 | fn char_width(c: char) -> usize { | ||
| 166 | let cp = c as u32; | ||
| 167 | if cp < 0x20 || cp == 0x7F { | ||
| 168 | return 0; | ||
| 169 | } | ||
| 170 | if is_zero_width(cp) || is_combining(cp) { | ||
| 171 | return 0; | ||
| 172 | } | ||
| 173 | if is_wide(cp) { | ||
| 174 | 2 | ||
| 175 | } else { | ||
| 176 | 1 | ||
| 177 | } | ||
| 178 | } | ||
| 179 | |||
| 180 | fn is_zero_width(cp: u32) -> bool { | ||
| 181 | matches!( | ||
| 182 | cp, | ||
| 183 | 0x200B..=0x200F | 0xFE00..=0xFE0F | 0xFEFF | 0x1F3FB..=0x1F3FF | ||
| 184 | ) | ||
| 185 | } | ||
| 186 | |||
| 187 | fn is_combining(cp: u32) -> bool { | ||
| 188 | matches!( | ||
| 189 | cp, | ||
| 190 | 0x0300..=0x036F | ||
| 191 | | 0x1AB0..=0x1AFF | ||
| 192 | | 0x1DC0..=0x1DFF | ||
| 193 | | 0x20D0..=0x20FF | ||
| 194 | | 0xFE20..=0xFE2F | ||
| 195 | ) | ||
| 196 | } | ||
| 197 | |||
| 198 | fn is_wide(cp: u32) -> bool { | ||
| 199 | matches!( | ||
| 200 | cp, | ||
| 201 | 0x1100..=0x115F | ||
| 202 | | 0x2329..=0x232A | ||
| 203 | | 0x2E80..=0xA4CF | ||
| 204 | | 0xAC00..=0xD7A3 | ||
| 205 | | 0xF900..=0xFAFF | ||
| 206 | | 0xFE10..=0xFE19 | ||
| 207 | | 0xFE30..=0xFE6F | ||
| 208 | | 0xFF00..=0xFF60 | ||
| 209 | | 0xFFE0..=0xFFE6 | ||
| 210 | | 0x1F300..=0x1F64F | ||
| 211 | | 0x1F680..=0x1F6FF | ||
| 212 | | 0x1F900..=0x1F9FF | ||
| 213 | | 0x20000..=0x3FFFD | ||
| 214 | ) | ||
| 215 | } | ||
| 216 | |||
| 217 | /// The three lines printed on first contact with a host. | ||
| 218 | pub fn first_contact_lines(addr: &str, fingerprint: &str) -> Vec<String> { | ||
| 219 | vec![ | ||
| 220 | format!("First contact with {addr}: trusting a new certificate."), | ||
| 221 | format!("Fingerprint: {fingerprint}"), | ||
| 222 | "Future connections must present the same certificate key.".to_string(), | ||
| 223 | ] | ||
| 224 | } | ||
| 225 | |||
| 226 | /// Format remembered hosts so a 64-character fingerprint does not wrap on an | ||
| 227 | /// 80-column terminal. | ||
| 228 | pub fn host_lines(hosts: &[(String, Option<String>)]) -> Vec<String> { | ||
| 229 | if hosts.is_empty() { | ||
| 230 | return vec!["No remembered hosts.".to_string()]; | ||
| 231 | } | ||
| 232 | |||
| 233 | let mut lines = vec!["Remembered hosts:".to_string()]; | ||
| 234 | for (host, fingerprint) in hosts { | ||
| 235 | lines.push(format!(" {host}")); | ||
| 236 | match fingerprint { | ||
| 237 | Some(fingerprint) => lines.push(format!(" fingerprint: {fingerprint}")), | ||
| 238 | None => lines.push(" (no fingerprint)".to_string()), | ||
| 239 | } | ||
| 240 | } | ||
| 241 | lines | ||
| 242 | } | ||
| 243 | |||
| 156 | /// Lines to move the cursor up to reach the first line of a region that ends | 244 | /// Lines to move the cursor up to reach the first line of a region that ends |
| 157 | /// on the current line. `render()` has no trailing newline, so a 7-line board | 245 | /// on the current line. `render()` has no trailing newline, so a 7-line board |
| 158 | /// needs `MoveUp(6)`. | 246 | /// needs `MoveUp(6)`. |
| @@ -168,3 +256,8 @@ pub fn input_cursor_column(prompt_len: usize, input_len: usize) -> u16 { | |||
| 168 | .saturating_add(input_len) | 256 | .saturating_add(input_len) |
| 169 | .min(u16::MAX as usize) as u16 | 257 | .min(u16::MAX as usize) as u16 |
| 170 | } | 258 | } |
| 259 | |||
| 260 | /// Format a ping round-trip time with one decimal place. | ||
| 261 | pub fn format_pong(elapsed: std::time::Duration) -> String { | ||
| 262 | format!("Pong ({:.1}ms)", elapsed.as_secs_f64() * 1000.0) | ||
| 263 | } | ||
