1#![forbid(unsafe_code)]
2
3use crate::terminal_capabilities::{TerminalCapabilities, TerminalProfile};
11use crate::text_width;
12use unicode_width::UnicodeWidthChar;
13
14const ENV_GLYPH_MODE: &str = "FTUI_GLYPH_MODE";
16const ENV_GLYPH_EMOJI: &str = "FTUI_GLYPH_EMOJI";
18const ENV_NO_EMOJI: &str = "FTUI_NO_EMOJI";
20const ENV_GLYPH_LINE_DRAWING: &str = "FTUI_GLYPH_LINE_DRAWING";
22const ENV_GLYPH_ARROWS: &str = "FTUI_GLYPH_ARROWS";
24const ENV_GLYPH_DOUBLE_WIDTH: &str = "FTUI_GLYPH_DOUBLE_WIDTH";
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum GlyphMode {
30 Unicode,
32 Ascii,
34}
35
36impl GlyphMode {
37 fn parse(value: &str) -> Option<Self> {
38 match value.trim().to_ascii_lowercase().as_str() {
39 "unicode" | "uni" | "u" => Some(Self::Unicode),
40 "ascii" | "ansi" | "a" => Some(Self::Ascii),
41 _ => None,
42 }
43 }
44
45 #[must_use]
46 pub const fn as_str(self) -> &'static str {
47 match self {
48 Self::Unicode => "unicode",
49 Self::Ascii => "ascii",
50 }
51 }
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub struct GlyphPolicy {
57 pub mode: GlyphMode,
59 pub emoji: bool,
61 pub cjk_width: bool,
63 pub double_width: bool,
65 pub unicode_box_drawing: bool,
67 pub unicode_line_drawing: bool,
69 pub unicode_arrows: bool,
71}
72
73impl GlyphPolicy {
74 #[must_use]
76 pub fn detect() -> Self {
77 let caps = TerminalCapabilities::with_overrides();
78 Self::from_env_with(|key| std::env::var(key).ok(), &caps)
79 }
80
81 #[must_use]
83 pub fn from_env_with<F>(get_env: F, caps: &TerminalCapabilities) -> Self
84 where
85 F: Fn(&str) -> Option<String>,
86 {
87 let mode = detect_mode(&get_env, caps);
88 let (mut emoji, emoji_overridden) = detect_emoji(&get_env, caps, mode);
89 let double_width = detect_double_width(&get_env, caps);
90 let mut cjk_width = text_width::cjk_width_from_env(|key| get_env(key));
91 if !double_width {
92 cjk_width = false;
93 }
94 if !double_width && !emoji_overridden {
95 emoji = false;
96 }
97
98 let unicode_box_drawing = caps.unicode_box_drawing;
99 let mut unicode_line_drawing = mode == GlyphMode::Unicode && unicode_box_drawing;
100 if let Some(value) = env_override_bool(&get_env, ENV_GLYPH_LINE_DRAWING) {
101 unicode_line_drawing = value;
102 }
103 if mode == GlyphMode::Ascii {
104 unicode_line_drawing = false;
105 }
106 if unicode_line_drawing && !glyphs_fit_narrow(LINE_DRAWING_GLYPHS, cjk_width) {
107 unicode_line_drawing = false;
108 }
109
110 let mut unicode_arrows = mode == GlyphMode::Unicode;
111 if let Some(value) = env_override_bool(&get_env, ENV_GLYPH_ARROWS) {
112 unicode_arrows = value;
113 }
114 if mode == GlyphMode::Ascii {
115 unicode_arrows = false;
116 }
117 if unicode_arrows && !glyphs_fit_narrow(ARROW_GLYPHS, cjk_width) {
118 unicode_arrows = false;
119 }
120
121 Self {
122 mode,
123 emoji,
124 cjk_width,
125 double_width,
126 unicode_box_drawing,
127 unicode_line_drawing,
128 unicode_arrows,
129 }
130 }
131
132 #[must_use]
134 pub fn to_json(&self) -> String {
135 format!(
136 concat!(
137 r#"{{"glyph_mode":"{}","emoji":{},"cjk_width":{},"double_width":{},"unicode_box_drawing":{},"unicode_line_drawing":{},"unicode_arrows":{}}}"#
138 ),
139 self.mode.as_str(),
140 self.emoji,
141 self.cjk_width,
142 self.double_width,
143 self.unicode_box_drawing,
144 self.unicode_line_drawing,
145 self.unicode_arrows
146 )
147 }
148}
149
150const LINE_DRAWING_GLYPHS: &[char] = &[
151 '─', '│', '┌', '┐', '└', '┘', '┬', '┴', '├', '┤', '┼', '╭', '╮', '╯', '╰',
152];
153const ARROW_GLYPHS: &[char] = &['→', '←', '↑', '↓', '↔', '↕', '⇢', '⇠', '⇡', '⇣'];
154
155fn detect_mode<F>(get_env: &F, caps: &TerminalCapabilities) -> GlyphMode
156where
157 F: Fn(&str) -> Option<String>,
158{
159 if let Some(value) = get_env(ENV_GLYPH_MODE)
160 && let Some(parsed) = GlyphMode::parse(&value)
161 {
162 return parsed;
163 }
164
165 if !caps.unicode_box_drawing {
166 return GlyphMode::Ascii;
167 }
168
169 match caps.profile() {
170 TerminalProfile::Dumb | TerminalProfile::Vt100 | TerminalProfile::LinuxConsole => {
171 GlyphMode::Ascii
172 }
173 _ => GlyphMode::Unicode,
174 }
175}
176
177fn detect_emoji<F>(get_env: &F, caps: &TerminalCapabilities, mode: GlyphMode) -> (bool, bool)
178where
179 F: Fn(&str) -> Option<String>,
180{
181 if mode == GlyphMode::Ascii {
182 return (false, false);
183 }
184
185 if let Some(value) = env_override_bool(get_env, ENV_GLYPH_EMOJI) {
186 return (value, true);
187 }
188
189 if let Some(value) = env_override_bool(get_env, ENV_NO_EMOJI) {
190 return (!value, true);
191 }
192
193 if !caps.unicode_emoji {
194 return (false, false);
195 }
196
197 (true, false)
199}
200
201fn detect_double_width<F>(get_env: &F, caps: &TerminalCapabilities) -> bool
202where
203 F: Fn(&str) -> Option<String>,
204{
205 if let Some(value) = env_override_bool(get_env, ENV_GLYPH_DOUBLE_WIDTH) {
206 return value;
207 }
208 caps.double_width
209}
210
211fn parse_bool(value: &str) -> Option<bool> {
212 match value.trim().to_ascii_lowercase().as_str() {
213 "1" | "true" | "yes" | "on" => Some(true),
214 "0" | "false" | "no" | "off" => Some(false),
215 _ => None,
216 }
217}
218
219fn env_override_bool<F>(get_env: &F, key: &str) -> Option<bool>
220where
221 F: Fn(&str) -> Option<String>,
222{
223 get_env(key).and_then(|value| parse_bool(&value))
224}
225
226fn glyph_width(ch: char, cjk_width: bool) -> usize {
227 if ch.is_ascii() {
228 return match ch {
229 '\t' | '\n' | '\r' => 1,
230 ' '..='~' => 1,
231 _ => 0,
232 };
233 }
234 if cjk_width {
235 ch.width_cjk().unwrap_or(0)
236 } else {
237 ch.width().unwrap_or(0)
238 }
239}
240
241fn glyphs_fit_narrow(glyphs: &[char], cjk_width: bool) -> bool {
242 glyphs
243 .iter()
244 .all(|&glyph| glyph_width(glyph, cjk_width) == 1)
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250 use std::collections::HashMap;
251
252 fn map_env(pairs: &[(&str, &str)]) -> HashMap<String, String> {
253 pairs
254 .iter()
255 .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
256 .collect()
257 }
258
259 fn get_env<'a>(map: &'a HashMap<String, String>) -> impl Fn(&str) -> Option<String> + 'a {
260 move |key| map.get(key).cloned()
261 }
262
263 #[test]
264 fn glyph_mode_ascii_forces_ascii_policy() {
265 let env = map_env(&[(ENV_GLYPH_MODE, "ascii"), ("TERM", "xterm-256color")]);
266 let caps = TerminalCapabilities::modern();
267 let policy = GlyphPolicy::from_env_with(get_env(&env), &caps);
268
269 assert_eq!(policy.mode, GlyphMode::Ascii);
270 assert!(!policy.unicode_line_drawing);
271 assert!(!policy.unicode_arrows);
272 assert!(!policy.emoji);
273 }
274
275 #[test]
276 fn emoji_override_disable() {
277 let env = map_env(&[(ENV_GLYPH_EMOJI, "0"), ("TERM", "wezterm")]);
278 let caps = TerminalCapabilities::modern();
279 let policy = GlyphPolicy::from_env_with(get_env(&env), &caps);
280
281 assert!(!policy.emoji);
282 }
283
284 #[test]
285 fn legacy_no_emoji_override_disables() {
286 let env = map_env(&[(ENV_NO_EMOJI, "1"), ("TERM", "wezterm")]);
287 let caps = TerminalCapabilities::modern();
288 let policy = GlyphPolicy::from_env_with(get_env(&env), &caps);
289
290 assert!(!policy.emoji);
291 }
292
293 #[test]
294 fn glyph_emoji_override_wins_over_legacy_no_emoji() {
295 let env = map_env(&[
296 (ENV_GLYPH_EMOJI, "1"),
297 (ENV_NO_EMOJI, "1"),
298 ("TERM", "wezterm"),
299 ]);
300 let caps = TerminalCapabilities::modern();
301 let policy = GlyphPolicy::from_env_with(get_env(&env), &caps);
302
303 assert!(policy.emoji);
304 }
305
306 #[test]
307 fn emoji_default_true_for_modern_term() {
308 let env = map_env(&[("TERM", "xterm-256color")]);
309 let caps = TerminalCapabilities::modern();
310 let policy = GlyphPolicy::from_env_with(get_env(&env), &caps);
311
312 assert!(policy.emoji);
313 }
314
315 #[test]
316 fn cjk_width_respects_env_override() {
317 let env = map_env(&[("FTUI_TEXT_CJK_WIDTH", "1")]);
318 let caps = TerminalCapabilities::modern();
319 let policy = GlyphPolicy::from_env_with(get_env(&env), &caps);
320
321 assert!(policy.cjk_width);
322 }
323
324 #[test]
325 fn caps_disable_box_drawing_forces_ascii_mode() {
326 let env = map_env(&[]);
327 let mut caps = TerminalCapabilities::modern();
328 caps.unicode_box_drawing = false;
329 let policy = GlyphPolicy::from_env_with(get_env(&env), &caps);
330
331 assert_eq!(policy.mode, GlyphMode::Ascii);
332 assert!(!policy.unicode_line_drawing);
333 assert!(!policy.unicode_arrows);
334 }
335
336 #[test]
337 fn caps_disable_emoji_disables_emoji_policy() {
338 let env = map_env(&[]);
339 let mut caps = TerminalCapabilities::modern();
340 caps.unicode_emoji = false;
341 let policy = GlyphPolicy::from_env_with(get_env(&env), &caps);
342
343 assert!(!policy.emoji);
344 }
345
346 #[test]
347 fn line_drawing_env_override_disables_unicode_lines() {
348 let env = map_env(&[(ENV_GLYPH_LINE_DRAWING, "0")]);
349 let caps = TerminalCapabilities::modern();
350 let policy = GlyphPolicy::from_env_with(get_env(&env), &caps);
351
352 assert!(!policy.unicode_line_drawing);
353 }
354
355 #[test]
356 fn arrows_env_override_disables_unicode_arrows() {
357 let env = map_env(&[(ENV_GLYPH_ARROWS, "0"), ("TERM", "wezterm")]);
358 let caps = TerminalCapabilities::modern();
359 let policy = GlyphPolicy::from_env_with(get_env(&env), &caps);
360
361 assert!(!policy.unicode_arrows);
362 }
363
364 #[test]
365 fn ascii_mode_forces_arrows_off_even_if_override_true() {
366 let env = map_env(&[
367 (ENV_GLYPH_MODE, "ascii"),
368 (ENV_GLYPH_ARROWS, "1"),
369 ("TERM", "wezterm"),
370 ]);
371 let caps = TerminalCapabilities::modern();
372 let policy = GlyphPolicy::from_env_with(get_env(&env), &caps);
373
374 assert_eq!(policy.mode, GlyphMode::Ascii);
375 assert!(!policy.unicode_arrows);
376 }
377
378 #[test]
379 fn emoji_env_override_true_ignores_caps_and_double_width() {
380 let env = map_env(&[(ENV_GLYPH_EMOJI, "1"), ("TERM", "dumb")]);
381 let mut caps = TerminalCapabilities::modern();
382 caps.unicode_emoji = false;
383 caps.double_width = false;
384 let policy = GlyphPolicy::from_env_with(get_env(&env), &caps);
385
386 assert!(policy.emoji);
387 }
388
389 #[test]
390 fn policy_to_json_serializes_expected_flags() {
391 let env = map_env(&[
392 (ENV_GLYPH_MODE, "unicode"),
393 (ENV_GLYPH_EMOJI, "0"),
394 (ENV_GLYPH_LINE_DRAWING, "1"),
395 (ENV_GLYPH_ARROWS, "0"),
396 (ENV_GLYPH_DOUBLE_WIDTH, "1"),
397 ("FTUI_TEXT_CJK_WIDTH", "1"),
398 ]);
399 let caps = TerminalCapabilities::modern();
400 let policy = GlyphPolicy::from_env_with(get_env(&env), &caps);
401
402 assert_eq!(
403 policy.to_json(),
404 r#"{"glyph_mode":"unicode","emoji":false,"cjk_width":true,"double_width":true,"unicode_box_drawing":true,"unicode_line_drawing":false,"unicode_arrows":false}"#
405 );
406 }
407
408 #[test]
409 fn glyph_double_width_env_overrides_cjk_width() {
410 let env = map_env(&[(ENV_GLYPH_DOUBLE_WIDTH, "0"), ("FTUI_TEXT_CJK_WIDTH", "1")]);
411 let caps = TerminalCapabilities::modern();
412 let policy = GlyphPolicy::from_env_with(get_env(&env), &caps);
413
414 assert!(!policy.cjk_width);
415 }
416
417 #[test]
418 fn caps_double_width_false_disables_cjk_width() {
419 let env = map_env(&[("FTUI_TEXT_CJK_WIDTH", "1")]);
420 let mut caps = TerminalCapabilities::modern();
421 caps.double_width = false;
422 let policy = GlyphPolicy::from_env_with(get_env(&env), &caps);
423
424 assert!(!policy.cjk_width);
425 }
426
427 #[test]
428 fn glyph_mode_parse_aliases() {
429 assert_eq!(GlyphMode::parse("uni"), Some(GlyphMode::Unicode));
430 assert_eq!(GlyphMode::parse("u"), Some(GlyphMode::Unicode));
431 assert_eq!(GlyphMode::parse("ansi"), Some(GlyphMode::Ascii));
432 assert_eq!(GlyphMode::parse("a"), Some(GlyphMode::Ascii));
433 assert_eq!(GlyphMode::parse("invalid"), None);
434 }
435
436 #[test]
437 fn glyph_mode_as_str_roundtrip() {
438 assert_eq!(GlyphMode::Unicode.as_str(), "unicode");
439 assert_eq!(GlyphMode::Ascii.as_str(), "ascii");
440 assert_eq!(
441 GlyphMode::parse(GlyphMode::Unicode.as_str()),
442 Some(GlyphMode::Unicode)
443 );
444 }
445
446 #[test]
447 fn parse_bool_truthy_and_falsy() {
448 assert_eq!(parse_bool("1"), Some(true));
449 assert_eq!(parse_bool("yes"), Some(true));
450 assert_eq!(parse_bool("on"), Some(true));
451 assert_eq!(parse_bool("0"), Some(false));
452 assert_eq!(parse_bool("no"), Some(false));
453 assert_eq!(parse_bool("off"), Some(false));
454 assert_eq!(parse_bool("garbage"), None);
455 }
456
457 #[test]
458 fn double_width_false_suppresses_emoji_without_explicit_override() {
459 let env = map_env(&[]);
460 let mut caps = TerminalCapabilities::modern();
461 caps.double_width = false;
462 let policy = GlyphPolicy::from_env_with(get_env(&env), &caps);
463
464 assert!(!policy.emoji);
465 }
466
467 #[test]
470 fn glyph_width_ascii_printable() {
471 assert_eq!(glyph_width('a', false), 1);
472 assert_eq!(glyph_width('Z', false), 1);
473 assert_eq!(glyph_width(' ', false), 1);
474 assert_eq!(glyph_width('~', false), 1);
475 }
476
477 #[test]
478 fn glyph_width_ascii_control_chars() {
479 assert_eq!(glyph_width('\t', false), 1);
480 assert_eq!(glyph_width('\n', false), 1);
481 assert_eq!(glyph_width('\r', false), 1);
482 assert_eq!(glyph_width('\0', false), 0);
484 assert_eq!(glyph_width('\x01', false), 0);
485 assert_eq!(glyph_width('\x7f', false), 0); }
487
488 #[test]
489 fn glyph_width_cjk_ideograph() {
490 assert_eq!(glyph_width('中', false), 2);
492 assert_eq!(glyph_width('中', true), 2);
493 }
494
495 #[test]
496 fn glyph_width_box_drawing_non_cjk() {
497 assert_eq!(glyph_width('─', false), 1);
499 assert_eq!(glyph_width('│', false), 1);
500 assert_eq!(glyph_width('┌', false), 1);
501 }
502
503 #[test]
504 fn glyph_width_arrow_non_cjk() {
505 assert_eq!(glyph_width('→', false), 1);
507 assert_eq!(glyph_width('←', false), 1);
508 assert_eq!(glyph_width('↑', false), 1);
509 }
510
511 #[test]
514 fn line_drawing_glyphs_fit_narrow_non_cjk() {
515 assert!(glyphs_fit_narrow(LINE_DRAWING_GLYPHS, false));
516 }
517
518 #[test]
519 fn arrow_glyphs_fit_narrow_non_cjk() {
520 assert!(glyphs_fit_narrow(ARROW_GLYPHS, false));
521 }
522
523 #[test]
524 fn glyphs_fit_narrow_rejects_wide_char() {
525 assert!(!glyphs_fit_narrow(&['中'], false));
527 }
528
529 #[test]
530 fn glyphs_fit_narrow_empty_set() {
531 assert!(glyphs_fit_narrow(&[], false));
532 assert!(glyphs_fit_narrow(&[], true));
533 }
534
535 #[test]
538 fn dumb_terminal_defaults_to_ascii() {
539 let env = map_env(&[]);
540 let caps = TerminalCapabilities::dumb();
541 let policy = GlyphPolicy::from_env_with(get_env(&env), &caps);
542
543 assert_eq!(policy.mode, GlyphMode::Ascii);
544 }
545
546 #[test]
547 fn mode_env_override_unicode_on_dumb_term() {
548 let env = map_env(&[(ENV_GLYPH_MODE, "unicode")]);
549 let caps = TerminalCapabilities::dumb();
550 let policy = GlyphPolicy::from_env_with(get_env(&env), &caps);
551
552 assert_eq!(policy.mode, GlyphMode::Unicode);
553 }
554
555 #[test]
558 fn env_override_bool_missing_key_returns_none() {
559 let env = map_env(&[]);
560 assert!(env_override_bool(&get_env(&env), ENV_GLYPH_EMOJI).is_none());
561 }
562
563 #[test]
564 fn env_override_bool_invalid_value_returns_none() {
565 let env = map_env(&[(ENV_GLYPH_EMOJI, "maybe")]);
566 assert!(env_override_bool(&get_env(&env), ENV_GLYPH_EMOJI).is_none());
567 }
568
569 #[test]
572 fn line_drawing_enabled_with_env_override() {
573 let env = map_env(&[(ENV_GLYPH_LINE_DRAWING, "1")]);
574 let caps = TerminalCapabilities::modern();
575 let policy = GlyphPolicy::from_env_with(get_env(&env), &caps);
576
577 assert!(policy.unicode_line_drawing);
578 }
579
580 #[test]
581 fn arrows_enabled_with_env_override() {
582 let env = map_env(&[(ENV_GLYPH_ARROWS, "1")]);
583 let caps = TerminalCapabilities::modern();
584 let policy = GlyphPolicy::from_env_with(get_env(&env), &caps);
585
586 assert!(policy.unicode_arrows);
587 }
588
589 #[test]
590 fn ascii_mode_forces_line_drawing_off_even_with_override() {
591 let env = map_env(&[(ENV_GLYPH_MODE, "ascii"), (ENV_GLYPH_LINE_DRAWING, "1")]);
592 let caps = TerminalCapabilities::modern();
593 let policy = GlyphPolicy::from_env_with(get_env(&env), &caps);
594
595 assert_eq!(policy.mode, GlyphMode::Ascii);
596 assert!(!policy.unicode_line_drawing);
597 }
598
599 #[test]
600 fn double_width_env_override_true() {
601 let env = map_env(&[(ENV_GLYPH_DOUBLE_WIDTH, "1")]);
602 let mut caps = TerminalCapabilities::modern();
603 caps.double_width = false;
604 let policy = GlyphPolicy::from_env_with(get_env(&env), &caps);
605
606 assert!(policy.double_width);
607 }
608
609 #[test]
612 fn default_modern_policy() {
613 let env = map_env(&[]);
614 let caps = TerminalCapabilities::modern();
615 let policy = GlyphPolicy::from_env_with(get_env(&env), &caps);
616
617 assert_eq!(policy.mode, GlyphMode::Unicode);
618 assert!(policy.emoji);
619 assert!(policy.double_width);
620 assert!(policy.unicode_box_drawing);
621 assert!(policy.unicode_line_drawing);
622 assert!(policy.unicode_arrows);
623 }
624
625 #[test]
628 fn glyph_mode_parse_case_insensitive() {
629 assert_eq!(GlyphMode::parse("UNICODE"), Some(GlyphMode::Unicode));
630 assert_eq!(GlyphMode::parse("Unicode"), Some(GlyphMode::Unicode));
631 assert_eq!(GlyphMode::parse("ASCII"), Some(GlyphMode::Ascii));
632 assert_eq!(GlyphMode::parse("Ascii"), Some(GlyphMode::Ascii));
633 }
634
635 #[test]
636 fn glyph_mode_parse_whitespace_trimmed() {
637 assert_eq!(GlyphMode::parse(" unicode "), Some(GlyphMode::Unicode));
638 assert_eq!(GlyphMode::parse("\tascii\n"), Some(GlyphMode::Ascii));
639 }
640
641 #[test]
642 fn glyph_mode_parse_empty_returns_none() {
643 assert_eq!(GlyphMode::parse(""), None);
644 }
645
646 #[test]
649 fn parse_bool_case_insensitive() {
650 assert_eq!(parse_bool("TRUE"), Some(true));
651 assert_eq!(parse_bool("True"), Some(true));
652 assert_eq!(parse_bool("FALSE"), Some(false));
653 assert_eq!(parse_bool("False"), Some(false));
654 assert_eq!(parse_bool("YES"), Some(true));
655 assert_eq!(parse_bool("NO"), Some(false));
656 }
657
658 #[test]
659 fn parse_bool_whitespace_trimmed() {
660 assert_eq!(parse_bool(" true "), Some(true));
661 assert_eq!(parse_bool("\t0\n"), Some(false));
662 }
663
664 #[test]
667 fn to_json_all_true() {
668 let policy = GlyphPolicy {
669 mode: GlyphMode::Unicode,
670 emoji: true,
671 cjk_width: true,
672 double_width: true,
673 unicode_box_drawing: true,
674 unicode_line_drawing: true,
675 unicode_arrows: true,
676 };
677 let json = policy.to_json();
678 assert!(json.contains(r#""glyph_mode":"unicode""#));
679 assert!(json.contains(r#""emoji":true"#));
680 assert!(json.contains(r#""cjk_width":true"#));
681 assert!(json.contains(r#""double_width":true"#));
682 assert!(json.contains(r#""unicode_box_drawing":true"#));
683 assert!(json.contains(r#""unicode_line_drawing":true"#));
684 assert!(json.contains(r#""unicode_arrows":true"#));
685 }
686
687 #[test]
688 fn to_json_all_false_ascii() {
689 let policy = GlyphPolicy {
690 mode: GlyphMode::Ascii,
691 emoji: false,
692 cjk_width: false,
693 double_width: false,
694 unicode_box_drawing: false,
695 unicode_line_drawing: false,
696 unicode_arrows: false,
697 };
698 let json = policy.to_json();
699 assert!(json.contains(r#""glyph_mode":"ascii""#));
700 assert!(json.contains(r#""emoji":false"#));
701 }
702
703 #[test]
706 fn vt100_terminal_defaults_to_ascii() {
707 let env = map_env(&[]);
708 let caps = TerminalCapabilities::vt100();
709 let policy = GlyphPolicy::from_env_with(get_env(&env), &caps);
710
711 assert_eq!(policy.mode, GlyphMode::Ascii);
712 assert!(!policy.unicode_line_drawing);
713 assert!(!policy.unicode_arrows);
714 }
715
716 #[test]
717 fn linux_console_defaults_to_ascii_despite_box_drawing_caps() {
718 let env = map_env(&[]);
721 let caps = TerminalCapabilities::linux_console();
722 let policy = GlyphPolicy::from_env_with(get_env(&env), &caps);
723
724 assert!(caps.unicode_box_drawing);
725 assert_eq!(policy.mode, GlyphMode::Ascii);
726 assert!(!policy.unicode_line_drawing);
727 assert!(!policy.unicode_arrows);
728 assert!(!policy.emoji);
729 }
730
731 #[test]
732 fn mode_env_override_unicode_on_linux_console() {
733 let env = map_env(&[(ENV_GLYPH_MODE, "unicode")]);
734 let caps = TerminalCapabilities::linux_console();
735 let policy = GlyphPolicy::from_env_with(get_env(&env), &caps);
736
737 assert_eq!(policy.mode, GlyphMode::Unicode);
738 }
739
740 #[test]
743 fn legacy_no_emoji_false_enables_emoji() {
744 let env = map_env(&[(ENV_NO_EMOJI, "0")]);
746 let caps = TerminalCapabilities::modern();
747 let policy = GlyphPolicy::from_env_with(get_env(&env), &caps);
748
749 assert!(policy.emoji);
750 }
751
752 #[test]
753 fn emoji_disabled_in_ascii_mode_even_with_all_caps() {
754 let env = map_env(&[(ENV_GLYPH_MODE, "ascii")]);
755 let mut caps = TerminalCapabilities::modern();
756 caps.unicode_emoji = true;
757 let policy = GlyphPolicy::from_env_with(get_env(&env), &caps);
758
759 assert_eq!(policy.mode, GlyphMode::Ascii);
760 assert!(!policy.emoji);
761 }
762
763 #[test]
764 fn emoji_override_true_in_ascii_mode_still_disabled() {
765 let env = map_env(&[(ENV_GLYPH_MODE, "ascii"), (ENV_GLYPH_EMOJI, "1")]);
768 let caps = TerminalCapabilities::modern();
769 let policy = GlyphPolicy::from_env_with(get_env(&env), &caps);
770
771 assert!(!policy.emoji);
772 }
773
774 #[test]
777 fn glyph_width_box_drawing_cjk_mode() {
778 assert_eq!(glyph_width('─', true), 2);
781 assert_eq!(glyph_width('│', true), 2);
782 assert_eq!(glyph_width('┌', true), 2);
783 assert_eq!(glyph_width('╭', true), 2);
784 }
785
786 #[test]
787 fn glyph_width_arrows_cjk_mode() {
788 assert_eq!(glyph_width('→', true), 2);
790 assert_eq!(glyph_width('←', true), 2);
791 assert_eq!(glyph_width('↑', true), 2);
792 assert_eq!(glyph_width('↓', true), 2);
793 }
794
795 #[test]
796 fn glyph_width_combining_mark_zero_width() {
797 assert_eq!(glyph_width('\u{0300}', false), 0);
800 assert_eq!(glyph_width('\u{0300}', true), 0);
801 }
802
803 #[test]
804 fn glyph_width_cjk_mode_does_not_affect_ascii() {
805 assert_eq!(glyph_width('a', true), 1);
807 assert_eq!(glyph_width('Z', true), 1);
808 assert_eq!(glyph_width('\0', true), 0);
809 }
810
811 #[test]
814 fn line_drawing_glyphs_wide_in_cjk_mode() {
815 assert!(!glyphs_fit_narrow(LINE_DRAWING_GLYPHS, true));
818 }
819
820 #[test]
821 fn arrow_glyphs_wide_in_cjk_mode() {
822 assert!(!glyphs_fit_narrow(ARROW_GLYPHS, true));
824 }
825
826 #[test]
829 fn cjk_width_disables_line_drawing_in_unicode_mode() {
830 let env = map_env(&[("FTUI_TEXT_CJK_WIDTH", "1")]);
833 let caps = TerminalCapabilities::modern();
834 let policy = GlyphPolicy::from_env_with(get_env(&env), &caps);
835
836 assert_eq!(policy.mode, GlyphMode::Unicode);
837 assert!(policy.cjk_width);
838 assert!(!policy.unicode_line_drawing);
839 }
840
841 #[test]
842 fn cjk_width_disables_arrows_in_unicode_mode() {
843 let env = map_env(&[("FTUI_TEXT_CJK_WIDTH", "1")]);
846 let caps = TerminalCapabilities::modern();
847 let policy = GlyphPolicy::from_env_with(get_env(&env), &caps);
848
849 assert_eq!(policy.mode, GlyphMode::Unicode);
850 assert!(policy.cjk_width);
851 assert!(!policy.unicode_arrows);
852 }
853
854 #[test]
855 fn line_drawing_env_override_still_disabled_by_cjk_width() {
856 let env = map_env(&[(ENV_GLYPH_LINE_DRAWING, "1"), ("FTUI_TEXT_CJK_WIDTH", "1")]);
859 let caps = TerminalCapabilities::modern();
860 let policy = GlyphPolicy::from_env_with(get_env(&env), &caps);
861
862 assert!(!policy.unicode_line_drawing);
863 }
864
865 #[test]
866 fn arrows_env_override_still_disabled_by_cjk_width() {
867 let env = map_env(&[(ENV_GLYPH_ARROWS, "1"), ("FTUI_TEXT_CJK_WIDTH", "1")]);
869 let caps = TerminalCapabilities::modern();
870 let policy = GlyphPolicy::from_env_with(get_env(&env), &caps);
871
872 assert!(!policy.unicode_arrows);
873 }
874
875 #[test]
878 fn env_override_bool_truthy_values() {
879 for val in &["1", "true", "yes", "on", "TRUE", "YES", "ON"] {
880 let env = map_env(&[(ENV_GLYPH_EMOJI, val)]);
881 assert_eq!(
882 env_override_bool(&get_env(&env), ENV_GLYPH_EMOJI),
883 Some(true),
884 "expected Some(true) for {val:?}"
885 );
886 }
887 }
888
889 #[test]
890 fn env_override_bool_falsy_values() {
891 for val in &["0", "false", "no", "off", "FALSE", "NO", "OFF"] {
892 let env = map_env(&[(ENV_GLYPH_EMOJI, val)]);
893 assert_eq!(
894 env_override_bool(&get_env(&env), ENV_GLYPH_EMOJI),
895 Some(false),
896 "expected Some(false) for {val:?}"
897 );
898 }
899 }
900
901 #[test]
904 fn glyph_mode_is_copy() {
905 let mode = GlyphMode::Unicode;
906 let copy = mode;
907 assert_eq!(mode, copy);
908 }
909
910 #[test]
911 fn glyph_policy_is_copy() {
912 let policy = GlyphPolicy {
913 mode: GlyphMode::Unicode,
914 emoji: true,
915 cjk_width: false,
916 double_width: true,
917 unicode_box_drawing: true,
918 unicode_line_drawing: true,
919 unicode_arrows: true,
920 };
921 let copy = policy;
922 assert_eq!(policy, copy);
924 assert_eq!(policy.mode, copy.mode);
925 }
926
927 #[test]
930 fn to_json_mixed_flags() {
931 let policy = GlyphPolicy {
932 mode: GlyphMode::Unicode,
933 emoji: false,
934 cjk_width: true,
935 double_width: true,
936 unicode_box_drawing: true,
937 unicode_line_drawing: false,
938 unicode_arrows: true,
939 };
940 let json = policy.to_json();
941 assert!(json.contains(r#""glyph_mode":"unicode""#));
942 assert!(json.contains(r#""emoji":false"#));
943 assert!(json.contains(r#""cjk_width":true"#));
944 assert!(json.contains(r#""double_width":true"#));
945 assert!(json.contains(r#""unicode_box_drawing":true"#));
946 assert!(json.contains(r#""unicode_line_drawing":false"#));
947 assert!(json.contains(r#""unicode_arrows":true"#));
948 }
949
950 #[test]
953 fn full_policy_vt100_all_defaults() {
954 let env = map_env(&[]);
955 let caps = TerminalCapabilities::vt100();
956 let policy = GlyphPolicy::from_env_with(get_env(&env), &caps);
957
958 assert_eq!(policy.mode, GlyphMode::Ascii);
959 assert!(!policy.emoji);
960 assert!(!policy.cjk_width);
961 assert!(!policy.double_width);
962 assert!(!policy.unicode_box_drawing);
963 assert!(!policy.unicode_line_drawing);
964 assert!(!policy.unicode_arrows);
965 }
966
967 #[test]
968 fn full_policy_linux_console_all_defaults() {
969 let env = map_env(&[]);
970 let caps = TerminalCapabilities::linux_console();
971 let policy = GlyphPolicy::from_env_with(get_env(&env), &caps);
972
973 assert_eq!(policy.mode, GlyphMode::Ascii);
974 assert!(!policy.emoji);
975 assert!(!policy.cjk_width);
976 assert!(!policy.double_width);
977 assert!(policy.unicode_box_drawing); assert!(!policy.unicode_line_drawing); assert!(!policy.unicode_arrows);
980 }
981}