summaryrefslogtreecommitdiffstats
path: root/src/client/ui.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/ui.rs')
-rw-r--r--src/client/ui.rs93
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.
161pub fn display_width(text: &str) -> usize {
162 text.chars().map(char_width).sum()
163}
164
165fn 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
180fn is_zero_width(cp: u32) -> bool {
181 matches!(
182 cp,
183 0x200B..=0x200F | 0xFE00..=0xFE0F | 0xFEFF | 0x1F3FB..=0x1F3FF
184 )
185}
186
187fn 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
198fn 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.
218pub 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.
228pub 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.
261pub fn format_pong(elapsed: std::time::Duration) -> String {
262 format!("Pong ({:.1}ms)", elapsed.as_secs_f64() * 1000.0)
263}