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
59#[cfg(not(all(not(target_arch = "wasm32"), feature = "crossterm")))]
65pub mod terminal_session {
66 #[derive(Debug, Default, Clone, Copy)]
68 pub struct TerminalOutputGuard;
69
70 #[inline]
73 #[must_use]
74 pub fn terminal_output_lock() -> TerminalOutputGuard {
75 TerminalOutputGuard
76 }
77}
78
79pub mod shutdown_signal {
80 use std::sync::{
87 Mutex, OnceLock,
88 atomic::{AtomicI32, Ordering},
89 };
90
91 static PENDING_TERMINATION_SIGNAL: AtomicI32 = AtomicI32::new(0);
92
93 pub fn record_pending_termination_signal(signal: i32) {
98 let _ = PENDING_TERMINATION_SIGNAL.compare_exchange(
99 0,
100 signal,
101 Ordering::SeqCst,
102 Ordering::SeqCst,
103 );
104 }
105
106 #[must_use]
108 pub fn pending_termination_signal() -> Option<i32> {
109 match PENDING_TERMINATION_SIGNAL.load(Ordering::SeqCst) {
110 0 => None,
111 signal => Some(signal),
112 }
113 }
114
115 pub fn clear_pending_termination_signal() {
117 PENDING_TERMINATION_SIGNAL.store(0, Ordering::SeqCst);
118 }
119
120 #[doc(hidden)]
127 pub fn with_test_signal_serialization<R>(f: impl FnOnce() -> R) -> R {
128 static SIGNAL_TEST_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
129
130 let _guard = SIGNAL_TEST_LOCK
131 .get_or_init(|| Mutex::new(()))
132 .lock()
133 .expect("shutdown signal test lock poisoned");
134 clear_pending_termination_signal();
135 let result = f();
136 clear_pending_termination_signal();
137 result
138 }
139}
140
141#[cfg(feature = "caps-probe")]
142pub mod caps_probe;
143
144#[cfg(feature = "tracing")]
146pub use logging::{
147 debug, debug_span, error, error_span, info, info_span, trace, trace_span, warn, warn_span,
148};
149
150pub mod text_width {
151 use std::sync::OnceLock;
182
183 use unicode_display_width::width as unicode_display_width;
184 use unicode_segmentation::UnicodeSegmentation;
185 use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
186
187 #[inline]
188 fn env_flag(value: &str) -> bool {
189 matches!(
190 value.trim().to_ascii_lowercase().as_str(),
191 "1" | "true" | "yes" | "on"
192 )
193 }
194
195 #[inline]
196 fn is_cjk_locale(locale: &str) -> bool {
197 let lower = locale.trim().to_ascii_lowercase();
198 lower.starts_with("ja") || lower.starts_with("zh") || lower.starts_with("ko")
199 }
200
201 #[inline]
202 fn cjk_width_from_env_impl<F>(get_env: F) -> bool
203 where
204 F: Fn(&str) -> Option<String>,
205 {
206 if let Some(value) = get_env("FTUI_GLYPH_DOUBLE_WIDTH") {
207 return env_flag(&value);
208 }
209 if let Some(value) = get_env("FTUI_TEXT_CJK_WIDTH").or_else(|| get_env("FTUI_CJK_WIDTH")) {
210 return env_flag(&value);
211 }
212 if let Some(locale) = get_env("LC_CTYPE").or_else(|| get_env("LANG")) {
213 return is_cjk_locale(&locale);
214 }
215 false
216 }
217
218 #[inline]
219 fn use_cjk_width() -> bool {
220 static CJK_WIDTH: OnceLock<bool> = OnceLock::new();
221 *CJK_WIDTH.get_or_init(|| cjk_width_from_env_impl(|key| std::env::var(key).ok()))
222 }
223
224 #[inline]
231 fn trust_vs16_width() -> bool {
232 static TRUST: OnceLock<bool> = OnceLock::new();
233 *TRUST.get_or_init(|| {
234 std::env::var("FTUI_EMOJI_VS16_WIDTH")
235 .map(|v| v.eq_ignore_ascii_case("unicode") || v == "2")
236 .unwrap_or(false)
237 })
238 }
239
240 #[inline]
242 pub fn vs16_trust_from_env<F>(get_env: F) -> bool
243 where
244 F: Fn(&str) -> Option<String>,
245 {
246 get_env("FTUI_EMOJI_VS16_WIDTH")
247 .map(|v| v.eq_ignore_ascii_case("unicode") || v == "2")
248 .unwrap_or(false)
249 }
250
251 #[inline]
253 pub fn vs16_width_trusted() -> bool {
254 trust_vs16_width()
255 }
256
257 #[inline]
260 fn strip_vs16(grapheme: &str) -> Option<String> {
261 if grapheme.contains('\u{FE0F}') {
262 Some(grapheme.chars().filter(|&c| c != '\u{FE0F}').collect())
263 } else {
264 None
265 }
266 }
267
268 #[inline]
270 pub fn cjk_width_from_env<F>(get_env: F) -> bool
271 where
272 F: Fn(&str) -> Option<String>,
273 {
274 cjk_width_from_env_impl(get_env)
275 }
276
277 #[inline]
279 pub fn cjk_width_enabled() -> bool {
280 use_cjk_width()
281 }
282
283 #[inline]
284 fn ascii_display_width(text: &str) -> usize {
285 let mut width = 0;
286 for b in text.bytes() {
287 match b {
288 b'\t' | b'\n' | b'\r' => width += 1,
289 0x20..=0x7E => width += 1,
290 _ => {}
291 }
292 }
293 width
294 }
295
296 #[inline]
298 #[must_use]
299 pub fn ascii_width(text: &str) -> Option<usize> {
300 if text.bytes().all(|b| (0x20..=0x7E).contains(&b)) {
301 Some(text.len())
302 } else {
303 None
304 }
305 }
306
307 #[inline]
308 fn is_zero_width_codepoint(c: char) -> bool {
309 let u = c as u32;
310 matches!(u, 0x0000..=0x001F | 0x007F..=0x009F)
311 || matches!(u, 0x0300..=0x036F | 0x1AB0..=0x1AFF | 0x1DC0..=0x1DFF | 0x20D0..=0x20FF)
312 || matches!(u, 0xFE20..=0xFE2F)
313 || matches!(u, 0xFE00..=0xFE0F | 0xE0100..=0xE01EF)
314 || matches!(
315 u,
316 0x00AD
317 | 0x034F
318 | 0x180E
319 | 0x200B
320 | 0x200C
321 | 0x200D
322 | 0x200E
323 | 0x200F
324 | 0x2060
325 | 0xFEFF
326 )
327 || matches!(u, 0x202A..=0x202E | 0x2066..=0x2069 | 0x206A..=0x206F)
328 }
329
330 #[inline]
332 #[must_use]
333 pub fn grapheme_width(grapheme: &str) -> usize {
334 if grapheme.is_ascii() {
335 return ascii_display_width(grapheme);
336 }
337 if grapheme.chars().all(is_zero_width_codepoint) {
338 return 0;
339 }
340 if use_cjk_width() {
341 return grapheme.width_cjk();
342 }
343 if !trust_vs16_width()
347 && let Some(stripped) = strip_vs16(grapheme)
348 {
349 if stripped.is_empty() {
350 return 0;
351 }
352 return unicode_display_width(&stripped) as usize;
353 }
354 unicode_display_width(grapheme) as usize
355 }
356
357 #[inline]
359 #[must_use]
360 pub fn char_width(ch: char) -> usize {
361 if ch.is_ascii() {
362 return match ch {
363 '\t' | '\n' | '\r' => 1,
364 ' '..='~' => 1,
365 _ => 0,
366 };
367 }
368 if is_zero_width_codepoint(ch) {
369 return 0;
370 }
371 if use_cjk_width() {
372 ch.width_cjk().unwrap_or(0)
373 } else {
374 ch.width().unwrap_or(0)
375 }
376 }
377
378 #[inline]
380 #[must_use]
381 pub fn display_width(text: &str) -> usize {
382 if let Some(width) = ascii_width(text) {
383 return width;
384 }
385 if text.is_ascii() {
386 return ascii_display_width(text);
387 }
388 let cjk_width = use_cjk_width();
389 if !text.chars().any(is_zero_width_codepoint) {
390 if cjk_width {
391 return text.width_cjk();
392 }
393 return unicode_display_width(text) as usize;
394 }
395 text.graphemes(true).map(grapheme_width).sum()
396 }
397
398 #[cfg(test)]
399 mod tests {
400 use super::*;
401
402 #[test]
405 fn cjk_width_env_explicit_true() {
406 let get = |key: &str| match key {
407 "FTUI_GLYPH_DOUBLE_WIDTH" => Some("1".into()),
408 _ => None,
409 };
410 assert!(cjk_width_from_env(get));
411 }
412
413 #[test]
414 fn cjk_width_env_explicit_false() {
415 let get = |key: &str| match key {
416 "FTUI_GLYPH_DOUBLE_WIDTH" => Some("0".into()),
417 _ => None,
418 };
419 assert!(!cjk_width_from_env(get));
420 }
421
422 #[test]
423 fn cjk_width_env_text_cjk_key() {
424 let get = |key: &str| match key {
425 "FTUI_TEXT_CJK_WIDTH" => Some("true".into()),
426 _ => None,
427 };
428 assert!(cjk_width_from_env(get));
429 }
430
431 #[test]
432 fn cjk_width_env_fallback_key() {
433 let get = |key: &str| match key {
434 "FTUI_CJK_WIDTH" => Some("yes".into()),
435 _ => None,
436 };
437 assert!(cjk_width_from_env(get));
438 }
439
440 #[test]
441 fn cjk_width_env_japanese_locale() {
442 let get = |key: &str| match key {
443 "LC_CTYPE" => Some("ja_JP.UTF-8".into()),
444 _ => None,
445 };
446 assert!(cjk_width_from_env(get));
447 }
448
449 #[test]
450 fn cjk_width_env_chinese_locale() {
451 let get = |key: &str| match key {
452 "LANG" => Some("zh_CN.UTF-8".into()),
453 _ => None,
454 };
455 assert!(cjk_width_from_env(get));
456 }
457
458 #[test]
459 fn cjk_width_env_korean_locale() {
460 let get = |key: &str| match key {
461 "LC_CTYPE" => Some("ko_KR.UTF-8".into()),
462 _ => None,
463 };
464 assert!(cjk_width_from_env(get));
465 }
466
467 #[test]
468 fn cjk_width_env_english_locale_returns_false() {
469 let get = |key: &str| match key {
470 "LANG" => Some("en_US.UTF-8".into()),
471 _ => None,
472 };
473 assert!(!cjk_width_from_env(get));
474 }
475
476 #[test]
477 fn cjk_width_env_no_vars_returns_false() {
478 let get = |_: &str| -> Option<String> { None };
479 assert!(!cjk_width_from_env(get));
480 }
481
482 #[test]
483 fn cjk_width_env_glyph_overrides_locale() {
484 let get = |key: &str| match key {
486 "FTUI_GLYPH_DOUBLE_WIDTH" => Some("0".into()),
487 "LANG" => Some("ja_JP.UTF-8".into()),
488 _ => None,
489 };
490 assert!(!cjk_width_from_env(get));
491 }
492
493 #[test]
494 fn cjk_width_env_on_is_true() {
495 let get = |key: &str| match key {
496 "FTUI_GLYPH_DOUBLE_WIDTH" => Some("on".into()),
497 _ => None,
498 };
499 assert!(cjk_width_from_env(get));
500 }
501
502 #[test]
503 fn cjk_width_env_case_insensitive() {
504 let get = |key: &str| match key {
505 "FTUI_CJK_WIDTH" => Some("TRUE".into()),
506 _ => None,
507 };
508 assert!(cjk_width_from_env(get));
509 }
510
511 #[test]
514 fn vs16_trust_unicode_string() {
515 let get = |key: &str| match key {
516 "FTUI_EMOJI_VS16_WIDTH" => Some("unicode".into()),
517 _ => None,
518 };
519 assert!(vs16_trust_from_env(get));
520 }
521
522 #[test]
523 fn vs16_trust_value_2() {
524 let get = |key: &str| match key {
525 "FTUI_EMOJI_VS16_WIDTH" => Some("2".into()),
526 _ => None,
527 };
528 assert!(vs16_trust_from_env(get));
529 }
530
531 #[test]
532 fn vs16_trust_not_set() {
533 let get = |_: &str| -> Option<String> { None };
534 assert!(!vs16_trust_from_env(get));
535 }
536
537 #[test]
538 fn vs16_trust_other_value() {
539 let get = |key: &str| match key {
540 "FTUI_EMOJI_VS16_WIDTH" => Some("1".into()),
541 _ => None,
542 };
543 assert!(!vs16_trust_from_env(get));
544 }
545
546 #[test]
547 fn vs16_trust_case_insensitive() {
548 let get = |key: &str| match key {
549 "FTUI_EMOJI_VS16_WIDTH" => Some("UNICODE".into()),
550 _ => None,
551 };
552 assert!(vs16_trust_from_env(get));
553 }
554
555 #[test]
558 fn ascii_width_pure_ascii() {
559 assert_eq!(ascii_width("hello"), Some(5));
560 }
561
562 #[test]
563 fn ascii_width_empty() {
564 assert_eq!(ascii_width(""), Some(0));
565 }
566
567 #[test]
568 fn ascii_width_with_space() {
569 assert_eq!(ascii_width("hello world"), Some(11));
570 }
571
572 #[test]
573 fn ascii_width_non_ascii_returns_none() {
574 assert_eq!(ascii_width("héllo"), None);
575 }
576
577 #[test]
578 fn ascii_width_with_tab_returns_none() {
579 assert_eq!(ascii_width("hello\tworld"), None);
581 }
582
583 #[test]
584 fn ascii_width_with_newline_returns_none() {
585 assert_eq!(ascii_width("hello\n"), None);
586 }
587
588 #[test]
589 fn ascii_width_control_char_returns_none() {
590 assert_eq!(ascii_width("\x01"), None);
591 }
592
593 #[test]
596 fn char_width_ascii_letter() {
597 assert_eq!(char_width('A'), 1);
598 }
599
600 #[test]
601 fn char_width_space() {
602 assert_eq!(char_width(' '), 1);
603 }
604
605 #[test]
606 fn char_width_tab() {
607 assert_eq!(char_width('\t'), 1);
608 }
609
610 #[test]
611 fn char_width_newline() {
612 assert_eq!(char_width('\n'), 1);
613 }
614
615 #[test]
616 fn char_width_nul() {
617 assert_eq!(char_width('\0'), 0);
619 }
620
621 #[test]
622 fn char_width_bell() {
623 assert_eq!(char_width('\x07'), 0);
625 }
626
627 #[test]
628 fn char_width_combining_accent() {
629 assert_eq!(char_width('\u{0301}'), 0);
631 }
632
633 #[test]
634 fn char_width_zwj() {
635 assert_eq!(char_width('\u{200D}'), 0);
637 }
638
639 #[test]
640 fn char_width_zwnbsp() {
641 assert_eq!(char_width('\u{FEFF}'), 0);
643 }
644
645 #[test]
646 fn char_width_soft_hyphen() {
647 assert_eq!(char_width('\u{00AD}'), 0);
649 }
650
651 #[test]
652 fn char_width_wide_east_asian() {
653 assert_eq!(char_width('⚡'), 2);
655 }
656
657 #[test]
658 fn char_width_cjk_ideograph() {
659 assert_eq!(char_width('中'), 2);
661 }
662
663 #[test]
664 fn char_width_variation_selector() {
665 assert_eq!(char_width('\u{FE0F}'), 0);
667 }
668
669 #[test]
672 fn display_width_ascii() {
673 assert_eq!(display_width("hello"), 5);
674 }
675
676 #[test]
677 fn display_width_empty() {
678 assert_eq!(display_width(""), 0);
679 }
680
681 #[test]
682 fn display_width_cjk_chars() {
683 assert_eq!(display_width("中文"), 4);
685 }
686
687 #[test]
688 fn display_width_mixed_ascii_cjk() {
689 assert_eq!(display_width("a中b"), 4);
691 }
692
693 #[test]
694 fn display_width_combining_chars() {
695 assert_eq!(display_width("e\u{0301}"), 1);
697 }
698
699 #[test]
700 fn display_width_ascii_with_control_codes() {
701 assert_eq!(display_width("a\tb"), 3);
704 }
705
706 #[test]
709 fn grapheme_width_ascii_char() {
710 assert_eq!(grapheme_width("A"), 1);
711 }
712
713 #[test]
714 fn grapheme_width_cjk_ideograph() {
715 assert_eq!(grapheme_width("中"), 2);
716 }
717
718 #[test]
719 fn grapheme_width_combining_sequence() {
720 assert_eq!(grapheme_width("e\u{0301}"), 1);
722 }
723
724 #[test]
725 fn grapheme_width_zwj_cluster() {
726 assert_eq!(grapheme_width("\u{200D}"), 0);
728 }
729 }
730}