A simple TUI Library written in Rust
1use std::env;
2use std::sync::atomic::{AtomicBool, Ordering};
3
4/// Level of color support detected in the terminal.
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum ColorLevel {
7 None,
8 Basic,
9 Color256,
10 TrueColor,
11}
12
13/// Get the terminal size as (columns, rows).
14/// Returns `None` if stdout is not a terminal.
15pub fn get_size() -> Option<(u16, u16)> {
16 unsafe {
17 let mut ws: libc::winsize = std::mem::zeroed();
18 let ret = libc::ioctl(libc::STDOUT_FILENO, libc::TIOCGWINSZ, &mut ws);
19 if ret == 0 && ws.ws_col > 0 && ws.ws_row > 0 {
20 Some((ws.ws_col, ws.ws_row))
21 } else {
22 None
23 }
24 }
25}
26
27/// Returns true if stdout is connected to a terminal (TTY).
28pub fn is_tty() -> bool {
29 unsafe { libc::isatty(libc::STDOUT_FILENO) != 0 }
30}
31
32/// Detect the color support level of the terminal.
33/// Checks NO_COLOR, COLORTERM, and TERM environment variables.
34pub fn supports_color() -> ColorLevel {
35 if env::var("NO_COLOR").is_ok() {
36 return ColorLevel::None;
37 }
38 if !is_tty() {
39 return ColorLevel::None;
40 }
41 detect_color_level()
42}
43
44// ---------------------------------------------------------------------------
45// SIGWINCH resize detection
46// ---------------------------------------------------------------------------
47
48/// Set to `true` by the SIGWINCH signal handler when the terminal is resized.
49static RESIZE_PENDING: AtomicBool = AtomicBool::new(false);
50
51/// The SIGWINCH signal handler. Only performs an atomic store — async-signal-safe.
52extern "C" fn sigwinch_handler(_: libc::c_int) {
53 RESIZE_PENDING.store(true, Ordering::Relaxed);
54}
55
56/// Install a SIGWINCH handler that sets an internal flag on terminal resize.
57/// Idempotent — safe to call multiple times.
58pub fn install_resize_handler() {
59 unsafe {
60 libc::signal(libc::SIGWINCH, sigwinch_handler as libc::sighandler_t);
61 }
62}
63
64/// Returns `true` and clears the pending flag if a terminal resize occurred
65/// since the last call to this function (or since `install_resize_handler`).
66pub fn take_resize() -> bool {
67 RESIZE_PENDING.swap(false, Ordering::Relaxed)
68}
69
70fn detect_color_level() -> ColorLevel {
71 if let Ok(ct) = env::var("COLORTERM")
72 && (ct == "truecolor" || ct == "24bit")
73 {
74 return ColorLevel::TrueColor;
75 }
76 if let Ok(term) = env::var("TERM") {
77 if term == "xterm-256color" || term == "screen-256color" || term.contains("256") {
78 return ColorLevel::Color256;
79 }
80 if term == "dumb" {
81 return ColorLevel::None;
82 }
83 return ColorLevel::Basic;
84 }
85 ColorLevel::Basic
86}