use std::env;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TerminalInfo {
pub is_tty: bool,
pub width: usize,
pub height: usize,
pub color_support: ColorSupport,
pub is_ci: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ColorSupport {
None,
Ansi16,
Ansi256,
TrueColor,
}
impl TerminalInfo {
pub fn detect() -> Self {
Self::detect_for_fd(1)
}
pub fn detect_for_fd(fd: i32) -> Self {
Self {
is_tty: is_tty(fd),
width: terminal_width_for_fd(fd),
height: terminal_height_for_fd(fd),
color_support: color_support(),
is_ci: is_ci_environment(),
}
}
}
pub fn terminal_width() -> usize {
terminal_width_for_fd(1)
}
pub fn terminal_width_for_fd(fd: i32) -> usize {
if let Some(width) = env_usize("COLUMNS") {
return width.max(1);
}
#[cfg(unix)]
{
if let Some(width) = unix_terminal_size(fd).map(|(w, _)| w) {
return width.max(1);
}
}
#[cfg(windows)]
{
if let Some((width, _)) = windows_terminal_size(fd) {
return width.max(1);
}
}
80
}
pub fn terminal_height() -> usize {
terminal_height_for_fd(1)
}
pub fn terminal_height_for_fd(fd: i32) -> usize {
if let Some(height) = env_usize("LINES") {
return height.max(1);
}
#[cfg(unix)]
{
if let Some(height) = unix_terminal_size(fd).map(|(_, h)| h) {
return height.max(1);
}
}
#[cfg(windows)]
{
if let Some((_, height)) = windows_terminal_size(fd) {
return height.max(1);
}
}
24
}
#[cfg(unix)]
pub fn is_tty(fd: i32) -> bool {
unsafe extern "C" {
fn isatty(fd: std::os::raw::c_int) -> std::os::raw::c_int;
}
unsafe { isatty(fd as std::os::raw::c_int) == 1 }
}
#[cfg(windows)]
pub fn is_tty(fd: i32) -> bool {
let Some(handle) = windows_output_handle(fd) else {
return false;
};
console_mode(handle).is_some()
}
#[cfg(unix)]
pub fn supports_interactive_output(fd: i32) -> bool {
is_tty(fd)
}
#[cfg(windows)]
pub fn supports_interactive_output(fd: i32) -> bool {
let Some(handle) = windows_output_handle(fd) else {
return false;
};
let Some(mode) = console_mode(handle) else {
return false;
};
const ENABLE_VIRTUAL_TERMINAL_PROCESSING: u32 = 0x0004;
if mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING != 0 {
return true;
}
unsafe { set_console_mode(handle, mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0 }
}
pub fn is_ci_environment() -> bool {
[
"CI",
"GITHUB_ACTIONS",
"GITLAB_CI",
"CIRCLECI",
"TRAVIS",
"JENKINS_URL",
"BUILDKITE",
"TF_BUILD",
"TEAMCITY_VERSION",
]
.iter()
.any(|key| env::var_os(key).is_some())
}
pub fn color_support() -> ColorSupport {
if env::var_os("NO_COLOR").is_some() {
return ColorSupport::None;
}
if let Ok(colorterm) = env::var("COLORTERM") {
let lower = colorterm.to_ascii_lowercase();
if lower == "truecolor" || lower == "24bit" {
return ColorSupport::TrueColor;
}
}
if let Ok(program) = env::var("TERM_PROGRAM")
&& (program == "iTerm.app" || program == "Hyper")
{
return ColorSupport::TrueColor;
}
if let Ok(term) = env::var("TERM") {
let lower = term.to_ascii_lowercase();
if lower.contains("256color") {
return ColorSupport::Ansi256;
}
if lower.contains("xterm") || lower.contains("ansi") || lower.contains("screen") {
return ColorSupport::Ansi16;
}
if lower == "dumb" {
return ColorSupport::None;
}
}
if is_ci_environment() {
ColorSupport::None
} else {
ColorSupport::Ansi16
}
}
fn env_usize(key: &str) -> Option<usize> {
env::var(key)
.ok()
.and_then(|value| value.parse::<usize>().ok())
}
#[cfg(windows)]
type Handle = *mut std::ffi::c_void;
#[cfg(windows)]
const STD_OUTPUT_HANDLE: u32 = -11i32 as u32;
#[cfg(windows)]
const STD_ERROR_HANDLE: u32 = -12i32 as u32;
#[cfg(windows)]
const INVALID_HANDLE_VALUE: Handle = -1isize as Handle;
#[cfg(windows)]
fn windows_output_handle(fd: i32) -> Option<Handle> {
let std_handle = match fd {
1 => STD_OUTPUT_HANDLE,
2 => STD_ERROR_HANDLE,
_ => return None,
};
let handle = unsafe { get_std_handle(std_handle) };
if handle.is_null() || handle == INVALID_HANDLE_VALUE {
return None;
}
Some(handle)
}
#[cfg(windows)]
fn console_mode(handle: Handle) -> Option<u32> {
let mut mode = 0u32;
let ok = unsafe { get_console_mode(handle, &mut mode) };
if ok == 0 { None } else { Some(mode) }
}
#[cfg(windows)]
fn windows_terminal_size(fd: i32) -> Option<(usize, usize)> {
let handle = windows_output_handle(fd)?;
let info = console_screen_buffer_info(handle)?;
let width = i32::from(info.sr_window.right) - i32::from(info.sr_window.left) + 1;
let height = i32::from(info.sr_window.bottom) - i32::from(info.sr_window.top) + 1;
if width > 0 && height > 0 {
Some((width as usize, height as usize))
} else {
None
}
}
#[cfg(windows)]
fn console_screen_buffer_info(handle: Handle) -> Option<ConsoleScreenBufferInfo> {
let mut info = ConsoleScreenBufferInfo::default();
let ok = unsafe { get_console_screen_buffer_info(handle, &mut info) };
if ok == 0 { None } else { Some(info) }
}
#[cfg(windows)]
#[derive(Clone, Copy, Default)]
#[repr(C)]
struct Coord {
x: i16,
y: i16,
}
#[cfg(windows)]
#[derive(Clone, Copy, Default)]
#[repr(C)]
struct SmallRect {
left: i16,
top: i16,
right: i16,
bottom: i16,
}
#[cfg(windows)]
#[derive(Clone, Copy, Default)]
#[repr(C)]
struct ConsoleScreenBufferInfo {
dw_size: Coord,
dw_cursor_position: Coord,
w_attributes: u16,
sr_window: SmallRect,
dw_maximum_window_size: Coord,
}
#[cfg(windows)]
unsafe fn get_std_handle(std_handle: u32) -> Handle {
unsafe extern "system" {
fn GetStdHandle(nStdHandle: u32) -> Handle;
}
unsafe { GetStdHandle(std_handle) }
}
#[cfg(windows)]
unsafe fn get_console_mode(handle: Handle, mode: *mut u32) -> i32 {
unsafe extern "system" {
fn GetConsoleMode(hConsoleHandle: Handle, lpMode: *mut u32) -> i32;
}
unsafe { GetConsoleMode(handle, mode) }
}
#[cfg(windows)]
unsafe fn set_console_mode(handle: Handle, mode: u32) -> i32 {
unsafe extern "system" {
fn SetConsoleMode(hConsoleHandle: Handle, dwMode: u32) -> i32;
}
unsafe { SetConsoleMode(handle, mode) }
}
#[cfg(windows)]
unsafe fn get_console_screen_buffer_info(
handle: Handle,
info: *mut ConsoleScreenBufferInfo,
) -> i32 {
unsafe extern "system" {
fn GetConsoleScreenBufferInfo(
hConsoleOutput: Handle,
lpConsoleScreenBufferInfo: *mut ConsoleScreenBufferInfo,
) -> i32;
}
unsafe { GetConsoleScreenBufferInfo(handle, info) }
}
#[cfg(unix)]
fn unix_terminal_size(fd: i32) -> Option<(usize, usize)> {
#[repr(C)]
struct WinSize {
ws_row: u16,
ws_col: u16,
ws_xpixel: u16,
ws_ypixel: u16,
}
unsafe extern "C" {
fn ioctl(
fd: std::os::raw::c_int,
request: std::os::raw::c_ulong,
...
) -> std::os::raw::c_int;
}
#[cfg(any(target_os = "linux", target_os = "android"))]
const TIOCGWINSZ: std::os::raw::c_ulong = 0x5413;
#[cfg(any(target_os = "macos", target_os = "ios", target_os = "freebsd"))]
const TIOCGWINSZ: std::os::raw::c_ulong = 0x4008_7468;
#[cfg(not(any(
target_os = "linux",
target_os = "android",
target_os = "macos",
target_os = "ios",
target_os = "freebsd"
)))]
const TIOCGWINSZ: std::os::raw::c_ulong = 0;
if TIOCGWINSZ == 0 {
return None;
}
let mut size = WinSize {
ws_row: 0,
ws_col: 0,
ws_xpixel: 0,
ws_ypixel: 0,
};
let result = unsafe { ioctl(fd as std::os::raw::c_int, TIOCGWINSZ, &mut size) };
if result == 0 && size.ws_col > 0 && size.ws_row > 0 {
Some((usize::from(size.ws_col), usize::from(size.ws_row)))
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
static ENV_LOCK: Mutex<()> = Mutex::new(());
fn with_env_lock(f: impl FnOnce()) {
match ENV_LOCK.lock() {
Ok(_guard) => f(),
Err(poisoned) => {
let _guard = poisoned.into_inner();
f();
}
}
}
#[test]
fn test_ci_detection_github_actions() {
with_env_lock(|| {
unsafe {
env::set_var("GITHUB_ACTIONS", "true");
}
assert!(is_ci_environment());
unsafe {
env::remove_var("GITHUB_ACTIONS");
}
});
}
#[test]
fn test_ci_detection_gitlab() {
with_env_lock(|| {
unsafe {
env::set_var("GITLAB_CI", "true");
}
assert!(is_ci_environment());
unsafe {
env::remove_var("GITLAB_CI");
}
});
}
#[test]
fn test_no_color_env() {
with_env_lock(|| {
unsafe {
env::set_var("NO_COLOR", "1");
}
assert_eq!(color_support(), ColorSupport::None);
unsafe {
env::remove_var("NO_COLOR");
}
});
}
#[test]
fn test_truecolor_detection() {
with_env_lock(|| {
unsafe {
env::remove_var("NO_COLOR");
env::set_var("COLORTERM", "truecolor");
}
assert_eq!(color_support(), ColorSupport::TrueColor);
unsafe {
env::remove_var("COLORTERM");
}
});
}
#[test]
fn test_256_color_detection() {
with_env_lock(|| {
unsafe {
env::remove_var("NO_COLOR");
env::remove_var("COLORTERM");
env::set_var("TERM", "xterm-256color");
}
assert_eq!(color_support(), ColorSupport::Ansi256);
unsafe {
env::remove_var("TERM");
}
});
}
#[test]
fn test_fallback_width_is_80() {
with_env_lock(|| {
unsafe {
env::remove_var("COLUMNS");
}
let width = terminal_width();
assert!(width == 80 || width > 0);
});
}
#[test]
fn test_columns_env_var_respected() {
with_env_lock(|| {
unsafe {
env::set_var("COLUMNS", "123");
}
assert_eq!(terminal_width(), 123);
assert_eq!(terminal_width_for_fd(2), 123);
unsafe {
env::remove_var("COLUMNS");
}
});
}
#[test]
fn test_invalid_fd_not_interactive() {
assert!(!is_tty(-1));
assert!(!supports_interactive_output(-1));
assert!(terminal_width_for_fd(-1) > 0);
assert!(terminal_height_for_fd(-1) > 0);
}
}