1pub const UTF8_LOCALE_ENVS: [&str; 3] = ["LC_ALL", "LC_CTYPE", "LANG"];
17
18#[must_use]
24pub fn unicode_env_enabled() -> bool {
25 UTF8_LOCALE_ENVS.iter().any(|key| {
26 std::env::var(key).is_ok_and(|value| {
27 let value = value.to_ascii_lowercase();
28 value.contains("utf-8") || value.contains("utf8")
29 })
30 })
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub struct Glyphs {
41 unicode: bool,
42}
43
44impl Glyphs {
45 #[must_use]
47 pub const fn new(unicode: bool) -> Self {
48 Self { unicode }
49 }
50
51 #[must_use]
53 pub fn from_env() -> Self {
54 Self::new(unicode_env_enabled())
55 }
56
57 #[must_use]
59 pub const fn unicode(self) -> bool {
60 self.unicode
61 }
62
63 #[must_use]
65 pub const fn success(self) -> &'static str {
66 self.pick("✓", "v")
67 }
68
69 #[must_use]
71 pub const fn error(self) -> &'static str {
72 self.pick("✗", "x")
73 }
74
75 #[must_use]
77 pub const fn warning(self) -> &'static str {
78 self.pick("⚠", "!")
79 }
80
81 #[must_use]
83 pub const fn info(self) -> &'static str {
84 self.pick("ℹ", "i")
85 }
86
87 #[must_use]
89 pub const fn bullet(self) -> &'static str {
90 self.pick("•", "*")
91 }
92
93 #[must_use]
95 pub const fn arrow(self) -> &'static str {
96 self.pick("→", "->")
97 }
98
99 #[must_use]
101 pub const fn pointer(self) -> &'static str {
102 self.pick("❯", ">")
103 }
104
105 #[must_use]
107 pub const fn answer(self) -> &'static str {
108 self.pick("»", ">")
109 }
110
111 #[must_use]
113 pub const fn radio_on(self) -> &'static str {
114 self.pick("◉", "(*)")
115 }
116
117 #[must_use]
119 pub const fn radio_off(self) -> &'static str {
120 self.pick("○", "( )")
121 }
122
123 #[must_use]
125 pub const fn arrow_up(self) -> &'static str {
126 self.pick("↑", "^")
127 }
128
129 #[must_use]
131 pub const fn arrow_down(self) -> &'static str {
132 self.pick("↓", "v")
133 }
134
135 #[must_use]
137 pub const fn ellipsis(self) -> &'static str {
138 self.pick("…", "...")
139 }
140
141 const fn pick(self, unicode: &'static str, ascii: &'static str) -> &'static str {
143 if self.unicode { unicode } else { ascii }
144 }
145}
146
147#[cfg(test)]
148mod tests {
149 use super::{Glyphs, unicode_env_enabled};
150
151 #[test]
152 fn env_probe_and_from_env_agree_and_are_callable() {
153 assert_eq!(Glyphs::from_env().unicode(), unicode_env_enabled());
157 }
158
159 #[test]
160 fn unicode_set_emits_symbols() {
161 let glyphs = Glyphs::new(true);
162 assert!(glyphs.unicode());
163 assert_eq!(glyphs.success(), "✓");
164 assert_eq!(glyphs.error(), "✗");
165 assert_eq!(glyphs.warning(), "⚠");
166 assert_eq!(glyphs.info(), "ℹ");
167 assert_eq!(glyphs.bullet(), "•");
168 assert_eq!(glyphs.arrow(), "→");
169 assert_eq!(glyphs.pointer(), "❯");
170 assert_eq!(glyphs.answer(), "»");
171 assert_eq!(glyphs.radio_on(), "◉");
172 assert_eq!(glyphs.radio_off(), "○");
173 assert_eq!(glyphs.arrow_up(), "↑");
174 assert_eq!(glyphs.arrow_down(), "↓");
175 assert_eq!(glyphs.ellipsis(), "…");
176 }
177
178 #[test]
179 fn ascii_fallback_is_byte_clean() {
180 let glyphs = Glyphs::new(false);
181 assert!(!glyphs.unicode());
182 for symbol in [
183 glyphs.success(),
184 glyphs.error(),
185 glyphs.warning(),
186 glyphs.info(),
187 glyphs.bullet(),
188 glyphs.arrow(),
189 glyphs.pointer(),
190 glyphs.answer(),
191 glyphs.radio_on(),
192 glyphs.radio_off(),
193 glyphs.arrow_up(),
194 glyphs.arrow_down(),
195 glyphs.ellipsis(),
196 ] {
197 assert!(symbol.is_ascii(), "fallback must be ASCII: {symbol:?}");
198 }
199 }
200}