use ratatui::symbols;
use ratatui::widgets::{Scrollbar, ScrollbarOrientation};
use std::borrow::Cow;
use std::io::Write;
use std::sync::OnceLock;
static UNICODE: OnceLock<bool> = OnceLock::new();
pub fn unicode() -> bool {
*UNICODE.get_or_init(locale_utf8)
}
pub fn detect() {
let ok = if std::env::var_os("TMUX").is_some() {
tmux_utf8().unwrap_or_else(locale_utf8)
} else {
probe().unwrap_or_else(locale_utf8)
};
let _ = UNICODE.set(ok);
}
fn tmux_utf8() -> Option<bool> {
use std::process::{Command, Stdio};
std::env::var_os("TMUX")?;
let mut child = Command::new("tmux")
.args(["display-message", "-p", "#{client_utf8}"])
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.ok()?;
let deadline = std::time::Instant::now() + std::time::Duration::from_millis(500);
while child.try_wait().ok()?.is_none() {
if std::time::Instant::now() >= deadline {
let _ = child.kill();
let _ = child.wait();
return None;
}
std::thread::sleep(std::time::Duration::from_millis(10));
}
let out = child.wait_with_output().ok()?;
match out.stdout.first().copied()? {
b'1' => Some(true),
b'0' => Some(false),
_ => None,
}
}
fn probe() -> Option<bool> {
use crossterm::{cursor, queue, terminal};
let mut out = std::io::stdout();
queue!(out, cursor::MoveTo(0, 0)).ok()?;
write!(out, "\u{2588}").ok()?; out.flush().ok()?;
let (col, _) = cursor::position().ok()?;
queue!(
out,
cursor::MoveTo(0, 0),
terminal::Clear(terminal::ClearType::All)
)
.ok()?;
out.flush().ok()?;
Some(col == 1)
}
fn locale_utf8() -> bool {
for k in ["LC_ALL", "LC_CTYPE", "LANG"] {
if let Ok(v) = std::env::var(k) {
if !v.is_empty() {
let v = v.to_ascii_uppercase();
return v.contains("UTF-8") || v.contains("UTF8");
}
}
}
false
}
fn ascii(c: char) -> Option<&'static str> {
Some(match c {
'│' | '▏' | '▐' => "|",
'─' => "-",
'↑' | '▲' => "^",
'↓' | '▼' => "v",
'←' | '◀' => "<",
'→' | '▶' => ">",
'●' => "*",
'▬' => "=",
'±' => "+-",
'…' => "...",
'█' | '▇' => "#",
'░' => "-",
'▆' | '▅' => "*",
'▄' | '▃' => "+",
'▂' => ".",
'▁' => "_",
_ => return None,
})
}
pub fn tr(s: &str) -> Cow<'_, str> {
if unicode() || s.is_ascii() {
return Cow::Borrowed(s);
}
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match ascii(c) {
Some(a) => out.push_str(a),
None => out.push(c),
}
}
Cow::Owned(out)
}
pub fn meter() -> (char, char) {
if unicode() {
('█', '░')
} else {
('|', ' ')
}
}
pub fn cursor() -> &'static str {
if unicode() {
"▶ "
} else {
"> "
}
}
pub fn border() -> symbols::border::Set<'static> {
if unicode() {
symbols::border::PLAIN
} else {
symbols::border::Set {
top_left: "+",
top_right: "+",
bottom_left: "+",
bottom_right: "+",
vertical_left: "|",
vertical_right: "|",
horizontal_top: "-",
horizontal_bottom: "-",
}
}
}
pub fn v_scrollbar() -> Scrollbar<'static> {
let (thumb, track, up, down) = if unicode() {
("▐", "│", "▲", "▼")
} else {
("#", "|", "^", "v")
};
Scrollbar::new(ScrollbarOrientation::VerticalRight)
.thumb_symbol(thumb)
.track_symbol(Some(track))
.begin_symbol(Some(up))
.end_symbol(Some(down))
}
pub fn h_scrollbar() -> Scrollbar<'static> {
let (thumb, track, left, right) = if unicode() {
("▬", "─", "◀", "▶")
} else {
("=", "-", "<", ">")
};
Scrollbar::new(ScrollbarOrientation::HorizontalBottom)
.thumb_symbol(thumb)
.track_symbol(Some(track))
.begin_symbol(Some(left))
.end_symbol(Some(right))
}
#[cfg(test)]
mod tests {
use super::ascii;
#[test]
fn ascii_map_covers_ui_glyphs() {
for c in [
'│', '─', '↑', '↓', '←', '→', '●', '▏', '▐', '▲', '▼', '◀', '▶', '▬', '±', '…', '█',
'▇', '▆', '▅', '▄', '▃', '▂', '▁', '░',
] {
let a = ascii(c).unwrap_or_else(|| panic!("no ascii for {c:?}"));
assert!(a.is_ascii(), "{c:?} -> {a:?} is not ascii");
}
}
}