1#![cfg_attr(not(test), forbid(unsafe_code))]
3#![cfg_attr(test, deny(unsafe_code))]
4
5pub mod animation;
24pub mod capability_override;
25pub mod cursor;
26pub mod cx;
27pub mod event;
28pub mod event_coalescer;
29pub mod generic_diff;
30pub mod generic_repr;
31pub mod geometry;
32pub mod gesture;
33pub mod glyph_policy;
34pub mod hover_stabilizer;
35pub mod inline_mode;
36pub mod input_parser;
37pub mod key_sequence;
38pub mod keybinding;
39pub mod logging;
40pub mod mode_typestate;
41pub mod mux_passthrough;
42pub mod read_optimized;
43pub mod s3_fifo;
44pub mod semantic_event;
45pub mod terminal_capabilities;
46#[cfg(all(not(target_arch = "wasm32"), feature = "crossterm"))]
47pub mod terminal_session;
48#[cfg(all(not(target_arch = "wasm32"), feature = "crossterm"))]
49pub use terminal_session::with_panic_cleanup_suppressed;
50#[cfg(not(all(not(target_arch = "wasm32"), feature = "crossterm")))]
51#[inline]
52pub fn with_panic_cleanup_suppressed<F, R>(f: F) -> R
53where
54 F: FnOnce() -> R,
55{
56 f()
57}
58
59pub mod shutdown_signal {
60 use std::sync::{
67 Mutex, OnceLock,
68 atomic::{AtomicI32, Ordering},
69 };
70
71 static PENDING_TERMINATION_SIGNAL: AtomicI32 = AtomicI32::new(0);
72
73 pub fn record_pending_termination_signal(signal: i32) {
78 let _ = PENDING_TERMINATION_SIGNAL.compare_exchange(
79 0,
80 signal,
81 Ordering::SeqCst,
82 Ordering::SeqCst,
83 );
84 }
85
86 #[must_use]
88 pub fn pending_termination_signal() -> Option<i32> {
89 match PENDING_TERMINATION_SIGNAL.load(Ordering::SeqCst) {
90 0 => None,
91 signal => Some(signal),
92 }
93 }
94
95 pub fn clear_pending_termination_signal() {
97 PENDING_TERMINATION_SIGNAL.store(0, Ordering::SeqCst);
98 }
99
100 #[doc(hidden)]
107 pub fn with_test_signal_serialization<R>(f: impl FnOnce() -> R) -> R {
108 static SIGNAL_TEST_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
109
110 let _guard = SIGNAL_TEST_LOCK
111 .get_or_init(|| Mutex::new(()))
112 .lock()
113 .expect("shutdown signal test lock poisoned");
114 clear_pending_termination_signal();
115 let result = f();
116 clear_pending_termination_signal();
117 result
118 }
119}
120
121#[cfg(feature = "caps-probe")]
122pub mod caps_probe;
123
124#[cfg(feature = "tracing")]
126pub use logging::{
127 debug, debug_span, error, error_span, info, info_span, trace, trace_span, warn, warn_span,
128};
129
130pub mod text_width {
131 use std::sync::OnceLock;
162
163 use unicode_display_width::width as unicode_display_width;
164 use unicode_segmentation::UnicodeSegmentation;
165 use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
166
167 #[inline]
168 fn env_flag(value: &str) -> bool {
169 matches!(
170 value.trim().to_ascii_lowercase().as_str(),
171 "1" | "true" | "yes" | "on"
172 )
173 }
174
175 #[inline]
176 fn is_cjk_locale(locale: &str) -> bool {
177 let lower = locale.trim().to_ascii_lowercase();
178 lower.starts_with("ja") || lower.starts_with("zh") || lower.starts_with("ko")
179 }
180
181 #[inline]
182 fn cjk_width_from_env_impl<F>(get_env: F) -> bool
183 where
184 F: Fn(&str) -> Option<String>,
185 {
186 if let Some(value) = get_env("FTUI_GLYPH_DOUBLE_WIDTH") {
187 return env_flag(&value);
188 }
189 if let Some(value) = get_env("FTUI_TEXT_CJK_WIDTH").or_else(|| get_env("FTUI_CJK_WIDTH")) {
190 return env_flag(&value);
191 }
192 if let Some(locale) = get_env("LC_CTYPE").or_else(|| get_env("LANG")) {
193 return is_cjk_locale(&locale);
194 }
195 false
196 }
197
198 #[inline]
199 fn use_cjk_width() -> bool {
200 static CJK_WIDTH: OnceLock<bool> = OnceLock::new();
201 *CJK_WIDTH.get_or_init(|| cjk_width_from_env_impl(|key| std::env::var(key).ok()))
202 }
203
204 #[inline]
211 fn trust_vs16_width() -> bool {
212 static TRUST: OnceLock<bool> = OnceLock::new();
213 *TRUST.get_or_init(|| {
214 std::env::var("FTUI_EMOJI_VS16_WIDTH")
215 .map(|v| v.eq_ignore_ascii_case("unicode") || v == "2")
216 .unwrap_or(false)
217 })
218 }
219
220 #[inline]
222 pub fn vs16_trust_from_env<F>(get_env: F) -> bool
223 where
224 F: Fn(&str) -> Option<String>,
225 {
226 get_env("FTUI_EMOJI_VS16_WIDTH")
227 .map(|v| v.eq_ignore_ascii_case("unicode") || v == "2")
228 .unwrap_or(false)
229 }
230
231 #[inline]
233 pub fn vs16_width_trusted() -> bool {
234 trust_vs16_width()
235 }
236
237 #[inline]
240 fn strip_vs16(grapheme: &str) -> Option<String> {
241 if grapheme.contains('\u{FE0F}') {
242 Some(grapheme.chars().filter(|&c| c != '\u{FE0F}').collect())
243 } else {
244 None
245 }
246 }
247
248 #[inline]
250 pub fn cjk_width_from_env<F>(get_env: F) -> bool
251 where
252 F: Fn(&str) -> Option<String>,
253 {
254 cjk_width_from_env_impl(get_env)
255 }
256
257 #[inline]
259 pub fn cjk_width_enabled() -> bool {
260 use_cjk_width()
261 }
262
263 #[inline]
264 fn ascii_display_width(text: &str) -> usize {
265 let mut width = 0;
266 for b in text.bytes() {
267 match b {
268 b'\t' | b'\n' | b'\r' => width += 1,
269 0x20..=0x7E => width += 1,
270 _ => {}
271 }
272 }
273 width
274 }
275
276 #[inline]
278 #[must_use]
279 pub fn ascii_width(text: &str) -> Option<usize> {
280 if text.bytes().all(|b| (0x20..=0x7E).contains(&b)) {
281 Some(text.len())
282 } else {
283 None
284 }
285 }
286
287 #[inline]
288 fn is_zero_width_codepoint(c: char) -> bool {
289 let u = c as u32;
290 matches!(u, 0x0000..=0x001F | 0x007F..=0x009F)
291 || matches!(u, 0x0300..=0x036F | 0x1AB0..=0x1AFF | 0x1DC0..=0x1DFF | 0x20D0..=0x20FF)
292 || matches!(u, 0xFE20..=0xFE2F)
293 || matches!(u, 0xFE00..=0xFE0F | 0xE0100..=0xE01EF)
294 || matches!(
295 u,
296 0x00AD
297 | 0x034F
298 | 0x180E
299 | 0x200B
300 | 0x200C
301 | 0x200D
302 | 0x200E
303 | 0x200F
304 | 0x2060
305 | 0xFEFF
306 )
307 || matches!(u, 0x202A..=0x202E | 0x2066..=0x2069 | 0x206A..=0x206F)
308 }
309
310 #[inline]
312 #[must_use]
313 pub fn grapheme_width(grapheme: &str) -> usize {
314 if grapheme.is_ascii() {
315 return ascii_display_width(grapheme);
316 }
317 if grapheme.chars().all(is_zero_width_codepoint) {
318 return 0;
319 }
320 if use_cjk_width() {
321 return grapheme.width_cjk();
322 }
323 if !trust_vs16_width()
327 && let Some(stripped) = strip_vs16(grapheme)
328 {
329 if stripped.is_empty() {
330 return 0;
331 }
332 return unicode_display_width(&stripped) as usize;
333 }
334 unicode_display_width(grapheme) as usize
335 }
336
337 #[inline]
339 #[must_use]
340 pub fn char_width(ch: char) -> usize {
341 if ch.is_ascii() {
342 return match ch {
343 '\t' | '\n' | '\r' => 1,
344 ' '..='~' => 1,
345 _ => 0,
346 };
347 }
348 if is_zero_width_codepoint(ch) {
349 return 0;
350 }
351 if use_cjk_width() {
352 ch.width_cjk().unwrap_or(0)
353 } else {
354 ch.width().unwrap_or(0)
355 }
356 }
357
358 #[inline]
360 #[must_use]
361 pub fn display_width(text: &str) -> usize {
362 if let Some(width) = ascii_width(text) {
363 return width;
364 }
365 if text.is_ascii() {
366 return ascii_display_width(text);
367 }
368 let cjk_width = use_cjk_width();
369 if !text.chars().any(is_zero_width_codepoint) {
370 if cjk_width {
371 return text.width_cjk();
372 }
373 return unicode_display_width(text) as usize;
374 }
375 text.graphemes(true).map(grapheme_width).sum()
376 }
377
378 #[cfg(test)]
379 mod tests {
380 use super::*;
381
382 #[test]
385 fn cjk_width_env_explicit_true() {
386 let get = |key: &str| match key {
387 "FTUI_GLYPH_DOUBLE_WIDTH" => Some("1".into()),
388 _ => None,
389 };
390 assert!(cjk_width_from_env(get));
391 }
392
393 #[test]
394 fn cjk_width_env_explicit_false() {
395 let get = |key: &str| match key {
396 "FTUI_GLYPH_DOUBLE_WIDTH" => Some("0".into()),
397 _ => None,
398 };
399 assert!(!cjk_width_from_env(get));
400 }
401
402 #[test]
403 fn cjk_width_env_text_cjk_key() {
404 let get = |key: &str| match key {
405 "FTUI_TEXT_CJK_WIDTH" => Some("true".into()),
406 _ => None,
407 };
408 assert!(cjk_width_from_env(get));
409 }
410
411 #[test]
412 fn cjk_width_env_fallback_key() {
413 let get = |key: &str| match key {
414 "FTUI_CJK_WIDTH" => Some("yes".into()),
415 _ => None,
416 };
417 assert!(cjk_width_from_env(get));
418 }
419
420 #[test]
421 fn cjk_width_env_japanese_locale() {
422 let get = |key: &str| match key {
423 "LC_CTYPE" => Some("ja_JP.UTF-8".into()),
424 _ => None,
425 };
426 assert!(cjk_width_from_env(get));
427 }
428
429 #[test]
430 fn cjk_width_env_chinese_locale() {
431 let get = |key: &str| match key {
432 "LANG" => Some("zh_CN.UTF-8".into()),
433 _ => None,
434 };
435 assert!(cjk_width_from_env(get));
436 }
437
438 #[test]
439 fn cjk_width_env_korean_locale() {
440 let get = |key: &str| match key {
441 "LC_CTYPE" => Some("ko_KR.UTF-8".into()),
442 _ => None,
443 };
444 assert!(cjk_width_from_env(get));
445 }
446
447 #[test]
448 fn cjk_width_env_english_locale_returns_false() {
449 let get = |key: &str| match key {
450 "LANG" => Some("en_US.UTF-8".into()),
451 _ => None,
452 };
453 assert!(!cjk_width_from_env(get));
454 }
455
456 #[test]
457 fn cjk_width_env_no_vars_returns_false() {
458 let get = |_: &str| -> Option<String> { None };
459 assert!(!cjk_width_from_env(get));
460 }
461
462 #[test]
463 fn cjk_width_env_glyph_overrides_locale() {
464 let get = |key: &str| match key {
466 "FTUI_GLYPH_DOUBLE_WIDTH" => Some("0".into()),
467 "LANG" => Some("ja_JP.UTF-8".into()),
468 _ => None,
469 };
470 assert!(!cjk_width_from_env(get));
471 }
472
473 #[test]
474 fn cjk_width_env_on_is_true() {
475 let get = |key: &str| match key {
476 "FTUI_GLYPH_DOUBLE_WIDTH" => Some("on".into()),
477 _ => None,
478 };
479 assert!(cjk_width_from_env(get));
480 }
481
482 #[test]
483 fn cjk_width_env_case_insensitive() {
484 let get = |key: &str| match key {
485 "FTUI_CJK_WIDTH" => Some("TRUE".into()),
486 _ => None,
487 };
488 assert!(cjk_width_from_env(get));
489 }
490
491 #[test]
494 fn vs16_trust_unicode_string() {
495 let get = |key: &str| match key {
496 "FTUI_EMOJI_VS16_WIDTH" => Some("unicode".into()),
497 _ => None,
498 };
499 assert!(vs16_trust_from_env(get));
500 }
501
502 #[test]
503 fn vs16_trust_value_2() {
504 let get = |key: &str| match key {
505 "FTUI_EMOJI_VS16_WIDTH" => Some("2".into()),
506 _ => None,
507 };
508 assert!(vs16_trust_from_env(get));
509 }
510
511 #[test]
512 fn vs16_trust_not_set() {
513 let get = |_: &str| -> Option<String> { None };
514 assert!(!vs16_trust_from_env(get));
515 }
516
517 #[test]
518 fn vs16_trust_other_value() {
519 let get = |key: &str| match key {
520 "FTUI_EMOJI_VS16_WIDTH" => Some("1".into()),
521 _ => None,
522 };
523 assert!(!vs16_trust_from_env(get));
524 }
525
526 #[test]
527 fn vs16_trust_case_insensitive() {
528 let get = |key: &str| match key {
529 "FTUI_EMOJI_VS16_WIDTH" => Some("UNICODE".into()),
530 _ => None,
531 };
532 assert!(vs16_trust_from_env(get));
533 }
534
535 #[test]
538 fn ascii_width_pure_ascii() {
539 assert_eq!(ascii_width("hello"), Some(5));
540 }
541
542 #[test]
543 fn ascii_width_empty() {
544 assert_eq!(ascii_width(""), Some(0));
545 }
546
547 #[test]
548 fn ascii_width_with_space() {
549 assert_eq!(ascii_width("hello world"), Some(11));
550 }
551
552 #[test]
553 fn ascii_width_non_ascii_returns_none() {
554 assert_eq!(ascii_width("héllo"), None);
555 }
556
557 #[test]
558 fn ascii_width_with_tab_returns_none() {
559 assert_eq!(ascii_width("hello\tworld"), None);
561 }
562
563 #[test]
564 fn ascii_width_with_newline_returns_none() {
565 assert_eq!(ascii_width("hello\n"), None);
566 }
567
568 #[test]
569 fn ascii_width_control_char_returns_none() {
570 assert_eq!(ascii_width("\x01"), None);
571 }
572
573 #[test]
576 fn char_width_ascii_letter() {
577 assert_eq!(char_width('A'), 1);
578 }
579
580 #[test]
581 fn char_width_space() {
582 assert_eq!(char_width(' '), 1);
583 }
584
585 #[test]
586 fn char_width_tab() {
587 assert_eq!(char_width('\t'), 1);
588 }
589
590 #[test]
591 fn char_width_newline() {
592 assert_eq!(char_width('\n'), 1);
593 }
594
595 #[test]
596 fn char_width_nul() {
597 assert_eq!(char_width('\0'), 0);
599 }
600
601 #[test]
602 fn char_width_bell() {
603 assert_eq!(char_width('\x07'), 0);
605 }
606
607 #[test]
608 fn char_width_combining_accent() {
609 assert_eq!(char_width('\u{0301}'), 0);
611 }
612
613 #[test]
614 fn char_width_zwj() {
615 assert_eq!(char_width('\u{200D}'), 0);
617 }
618
619 #[test]
620 fn char_width_zwnbsp() {
621 assert_eq!(char_width('\u{FEFF}'), 0);
623 }
624
625 #[test]
626 fn char_width_soft_hyphen() {
627 assert_eq!(char_width('\u{00AD}'), 0);
629 }
630
631 #[test]
632 fn char_width_wide_east_asian() {
633 assert_eq!(char_width('⚡'), 2);
635 }
636
637 #[test]
638 fn char_width_cjk_ideograph() {
639 assert_eq!(char_width('中'), 2);
641 }
642
643 #[test]
644 fn char_width_variation_selector() {
645 assert_eq!(char_width('\u{FE0F}'), 0);
647 }
648
649 #[test]
652 fn display_width_ascii() {
653 assert_eq!(display_width("hello"), 5);
654 }
655
656 #[test]
657 fn display_width_empty() {
658 assert_eq!(display_width(""), 0);
659 }
660
661 #[test]
662 fn display_width_cjk_chars() {
663 assert_eq!(display_width("中文"), 4);
665 }
666
667 #[test]
668 fn display_width_mixed_ascii_cjk() {
669 assert_eq!(display_width("a中b"), 4);
671 }
672
673 #[test]
674 fn display_width_combining_chars() {
675 assert_eq!(display_width("e\u{0301}"), 1);
677 }
678
679 #[test]
680 fn display_width_ascii_with_control_codes() {
681 assert_eq!(display_width("a\tb"), 3);
684 }
685
686 #[test]
689 fn grapheme_width_ascii_char() {
690 assert_eq!(grapheme_width("A"), 1);
691 }
692
693 #[test]
694 fn grapheme_width_cjk_ideograph() {
695 assert_eq!(grapheme_width("中"), 2);
696 }
697
698 #[test]
699 fn grapheme_width_combining_sequence() {
700 assert_eq!(grapheme_width("e\u{0301}"), 1);
702 }
703
704 #[test]
705 fn grapheme_width_zwj_cluster() {
706 assert_eq!(grapheme_width("\u{200D}"), 0);
708 }
709 }
710}