Skip to main content

cli_ui/
term.rs

1//! Terminal introspection — width detection and tty check.
2//!
3//! No external crates beyond what `anstream` already pulls in.
4
5/// Returns the terminal width in columns, or `80` as fallback.
6///
7/// Uses `TIOCGWINSZ` ioctl on Unix and `GetConsoleScreenBufferInfo` on Windows.
8/// Returns `80` when output is piped or size cannot be determined.
9pub fn width() -> usize {
10    terminal_width().unwrap_or(80)
11}
12
13/// Returns `true` if stderr is an interactive terminal.
14pub fn is_tty() -> bool {
15    use std::io::IsTerminal;
16    std::io::stderr().is_terminal()
17}
18
19#[cfg(unix)]
20fn terminal_width() -> Option<usize> {
21    // TIOCGWINSZ via inline libc call — no dependency needed
22    #[repr(C)]
23    struct Winsize {
24        rows: u16,
25        cols: u16,
26        _xpixel: u16,
27        _ypixel: u16,
28    }
29
30    let ws = Winsize {
31        rows: 0,
32        cols: 0,
33        _xpixel: 0,
34        _ypixel: 0,
35    };
36    // TIOCGWINSZ = 0x5413 on Linux, 0x40087468 on macOS
37    #[cfg(target_os = "macos")]
38    const TIOCGWINSZ: u64 = 0x40087468;
39    #[cfg(not(target_os = "macos"))]
40    const TIOCGWINSZ: u64 = 0x5413;
41
42    let ret = unsafe { libc_ioctl(STDERR_FILENO, TIOCGWINSZ, &ws as *const Winsize) };
43    if ret == 0 && ws.cols > 0 {
44        Some(ws.cols as usize)
45    } else {
46        None
47    }
48}
49
50#[cfg(unix)]
51extern "C" {
52    #[link_name = "ioctl"]
53    fn libc_ioctl(fd: i32, request: u64, ...) -> i32;
54}
55
56#[cfg(unix)]
57const STDERR_FILENO: i32 = 2;
58
59#[cfg(windows)]
60fn terminal_width() -> Option<usize> {
61    use windows_sys::Win32::System::Console::{
62        GetConsoleScreenBufferInfo, GetStdHandle, CONSOLE_SCREEN_BUFFER_INFO, STD_ERROR_HANDLE,
63    };
64    unsafe {
65        let handle = GetStdHandle(STD_ERROR_HANDLE);
66        if handle == 0 {
67            return None;
68        }
69        let mut info: CONSOLE_SCREEN_BUFFER_INFO = std::mem::zeroed();
70        if GetConsoleScreenBufferInfo(handle, &mut info) != 0 {
71            let w = info.srWindow.Right - info.srWindow.Left + 1;
72            if w > 0 {
73                Some(w as usize)
74            } else {
75                None
76            }
77        } else {
78            None
79        }
80    }
81}
82
83#[cfg(not(any(unix, windows)))]
84fn terminal_width() -> Option<usize> {
85    None
86}