r3bl_tui 0.7.2

TUI library to build modern apps inspired by React, Elm, with Flexbox, CSS, editor component, emoji support, and more
Documentation
/*
 *   Copyright (c) 2023-2025 R3BL LLC
 *   All rights reserved.
 *
 *   Licensed under the Apache License, Version 2.0 (the "License");
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *   limitations under the License.
 */

use std::io::IsTerminal;

use miette::IntoDiagnostic;

use crate::{height, width, ColWidth, Size};
pub const DEFAULT_WIDTH: u16 = 80;

#[must_use]
pub fn get_terminal_width_no_default() -> Option<ColWidth> {
    match get_size() {
        Ok(size) => Some(size.col_width),
        Err(_) => None,
    }
}

/// Get the terminal width. If there is a problem, return the default width.
#[must_use]
pub fn get_terminal_width() -> ColWidth {
    match get_size() {
        Ok(size) => size.col_width,
        Err(_) => width(DEFAULT_WIDTH),
    }
}

/// Get the terminal size.
///
/// # Errors
///
/// Returns an error if:
/// - The terminal size cannot be determined
/// - The terminal is not available or not a TTY
pub fn get_size() -> miette::Result<Size> {
    let (columns, rows) = crossterm::terminal::size().into_diagnostic()?;
    Ok(width(columns) + height(rows))
}

#[derive(Debug)]
pub enum StdinIsPipedResult {
    StdinIsPiped,
    StdinIsNotPiped,
}

#[derive(Debug)]
pub enum StdoutIsPipedResult {
    StdoutIsPiped,
    StdoutIsNotPiped,
}

/// If you run `echo "test" | cargo run` the following will return true.
/// More info: <https://unix.stackexchange.com/questions/597083/how-does-piping-affect-stdin>
#[must_use]
pub fn is_stdin_piped() -> StdinIsPipedResult {
    if std::io::stdin().is_terminal() {
        StdinIsPipedResult::StdinIsNotPiped
    } else {
        StdinIsPipedResult::StdinIsPiped
    }
}

/// If you run `cargo run | grep foo` the following will return true.
/// More info: <https://unix.stackexchange.com/questions/597083/how-does-piping-affect-stdin>
#[must_use]
pub fn is_stdout_piped() -> StdoutIsPipedResult {
    if std::io::stdout().is_terminal() {
        StdoutIsPipedResult::StdoutIsNotPiped
    } else {
        StdoutIsPipedResult::StdoutIsPiped
    }
}

#[derive(Debug, PartialEq, Clone, Copy)]
pub enum TTYResult {
    IsInteractive,
    IsNotInteractive,
}

/// Returns [`TTYResult::IsInteractive`] if stdin, stdout, and stderr are *all* fully
/// interactive.
///
/// There are situations where some can be interactive and others not, such as when piping
/// is active.
#[must_use]
pub fn is_fully_interactive_terminal() -> TTYResult {
    let is_tty: bool = std::io::stdin().is_terminal();
    if is_tty {
        TTYResult::IsInteractive
    } else {
        TTYResult::IsNotInteractive
    }
}

/// Returns [`TTYResult::IsNotInteractive`] if stdin, stdout, and stderr are *all* fully
/// uninteractive. This happens when `cargo test` runs.
///
/// There are situations where some can be interactive and others not, such as when piping
/// is active.
#[must_use]
pub fn is_fully_uninteractive_terminal() -> TTYResult {
    // Windows-specific workaround: When running through `cargo run` on Windows,
    // the terminal detection may incorrectly report all streams as non-terminal
    // even when running in an interactive terminal. This is because cargo may
    // redirect the streams. To work around this, we check if we're running
    // under cargo and if so, assume it's interactive.
    #[cfg(target_os = "windows")]
    if std::env::var("CARGO").is_ok() || std::env::var("CARGO_PKG_NAME").is_ok() {
        return TTYResult::IsInteractive;
    }
    
    let stdin_is_tty: bool = std::io::stdin().is_terminal();
    let stdout_is_tty: bool = std::io::stdout().is_terminal();
    let stderr_is_tty: bool = std::io::stderr().is_terminal();
    if !stdin_is_tty && !stdout_is_tty && !stderr_is_tty {
        TTYResult::IsNotInteractive
    } else {
        TTYResult::IsInteractive
    }
}

/// Return early if the terminal is not interactive.
///
/// This is primarily meant to be used in tests that run in a CI/CD environments, where
/// tests are run in a non-interactive terminal. It is meant to skip tests that require
/// user interaction. Tests marked with this should run on a local machine during
/// development.
///
/// However, it is also useful in other situations where you want to skip code that
/// requires user interaction.
#[macro_export]
macro_rules! return_if_not_interactive_terminal {
    ($result:expr) => {
        if let TTYResult::IsNotInteractive =
            $crate::term::is_fully_uninteractive_terminal()
        {
            return $result;
        }
    };
    () => {
        if let TTYResult::IsNotInteractive =
            $crate::term::is_fully_uninteractive_terminal()
        {
            return;
        }
    };
}