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 event;
27pub mod event_coalescer;
28pub mod geometry;
29pub mod gesture;
30pub mod glyph_policy;
31pub mod hover_stabilizer;
32pub mod inline_mode;
33pub mod input_parser;
34pub mod key_sequence;
35pub mod keybinding;
36pub mod logging;
37pub mod mux_passthrough;
38pub mod semantic_event;
39pub mod terminal_capabilities;
40pub mod terminal_session;
41
42#[cfg(feature = "caps-probe")]
43pub mod caps_probe;
44
45#[cfg(feature = "tracing")]
47pub use logging::{
48 debug, debug_span, error, error_span, info, info_span, trace, trace_span, warn, warn_span,
49};
50
51pub mod text_width {
52 use std::sync::OnceLock;
59
60 use unicode_display_width::width as unicode_display_width;
61 use unicode_segmentation::UnicodeSegmentation;
62 use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
63
64 #[inline]
65 fn env_flag(value: &str) -> bool {
66 matches!(
67 value.trim().to_ascii_lowercase().as_str(),
68 "1" | "true" | "yes" | "on"
69 )
70 }
71
72 #[inline]
73 fn is_cjk_locale(locale: &str) -> bool {
74 let lower = locale.trim().to_ascii_lowercase();
75 lower.starts_with("ja") || lower.starts_with("zh") || lower.starts_with("ko")
76 }
77
78 #[inline]
79 fn cjk_width_from_env_impl<F>(get_env: F) -> bool
80 where
81 F: Fn(&str) -> Option<String>,
82 {
83 if let Some(value) = get_env("FTUI_TEXT_CJK_WIDTH").or_else(|| get_env("FTUI_CJK_WIDTH")) {
84 return env_flag(&value);
85 }
86 if let Some(locale) = get_env("LC_CTYPE").or_else(|| get_env("LANG")) {
87 return is_cjk_locale(&locale);
88 }
89 false
90 }
91
92 #[inline]
93 fn use_cjk_width() -> bool {
94 static CJK_WIDTH: OnceLock<bool> = OnceLock::new();
95 *CJK_WIDTH.get_or_init(|| cjk_width_from_env_impl(|key| std::env::var(key).ok()))
96 }
97
98 #[inline]
100 pub fn cjk_width_from_env<F>(get_env: F) -> bool
101 where
102 F: Fn(&str) -> Option<String>,
103 {
104 cjk_width_from_env_impl(get_env)
105 }
106
107 #[inline]
109 pub fn cjk_width_enabled() -> bool {
110 use_cjk_width()
111 }
112
113 #[inline]
114 fn ascii_display_width(text: &str) -> usize {
115 let mut width = 0;
116 for b in text.bytes() {
117 match b {
118 b'\t' | b'\n' | b'\r' => width += 1,
119 0x20..=0x7E => width += 1,
120 _ => {}
121 }
122 }
123 width
124 }
125
126 #[inline]
128 #[must_use]
129 pub fn ascii_width(text: &str) -> Option<usize> {
130 if text.bytes().all(|b| (0x20..=0x7E).contains(&b)) {
131 Some(text.len())
132 } else {
133 None
134 }
135 }
136
137 #[inline]
138 fn is_zero_width_codepoint(c: char) -> bool {
139 let u = c as u32;
140 matches!(u, 0x0000..=0x001F | 0x007F..=0x009F)
141 || matches!(u, 0x0300..=0x036F | 0x1AB0..=0x1AFF | 0x1DC0..=0x1DFF | 0x20D0..=0x20FF)
142 || matches!(u, 0xFE20..=0xFE2F)
143 || matches!(u, 0xFE00..=0xFE0F | 0xE0100..=0xE01EF)
144 || matches!(
145 u,
146 0x00AD
147 | 0x034F
148 | 0x180E
149 | 0x200B
150 | 0x200C
151 | 0x200D
152 | 0x200E
153 | 0x200F
154 | 0x2060
155 | 0xFEFF
156 )
157 || matches!(u, 0x202A..=0x202E | 0x2066..=0x2069 | 0x206A..=0x206F)
158 }
159
160 #[inline]
162 #[must_use]
163 pub fn grapheme_width(grapheme: &str) -> usize {
164 if grapheme.is_ascii() {
165 return ascii_display_width(grapheme);
166 }
167 if grapheme.chars().all(is_zero_width_codepoint) {
168 return 0;
169 }
170 if use_cjk_width() {
171 return grapheme.width_cjk();
172 }
173 unicode_display_width(grapheme) as usize
174 }
175
176 #[inline]
178 #[must_use]
179 pub fn char_width(ch: char) -> usize {
180 if ch.is_ascii() {
181 return match ch {
182 '\t' | '\n' | '\r' => 1,
183 ' '..='~' => 1,
184 _ => 0,
185 };
186 }
187 if is_zero_width_codepoint(ch) {
188 return 0;
189 }
190 if use_cjk_width() {
191 ch.width_cjk().unwrap_or(0)
192 } else {
193 ch.width().unwrap_or(0)
194 }
195 }
196
197 #[inline]
199 #[must_use]
200 pub fn display_width(text: &str) -> usize {
201 if let Some(width) = ascii_width(text) {
202 return width;
203 }
204 if text.is_ascii() {
205 return ascii_display_width(text);
206 }
207 let cjk_width = use_cjk_width();
208 if !text.chars().any(is_zero_width_codepoint) {
209 if cjk_width {
210 return text.width_cjk();
211 }
212 return unicode_display_width(text) as usize;
213 }
214 text.graphemes(true).map(grapheme_width).sum()
215 }
216}