termal_core 5.0.0

This library contains implementation for the termal library
Documentation
use std::{
    ffi::c_void,
    fs,
    io::{self, ErrorKind},
    mem,
    os::fd::{AsRawFd, FromRawFd, IntoRawFd, OwnedFd, RawFd},
    ptr,
    sync::{Mutex, MutexGuard},
    time::{Duration, Instant},
};

use libc::{
    POLLERR, POLLIN, SIG_BLOCK, SIGWINCH, TCSANOW, TIOCGWINSZ, c_ulong,
    cfmakeraw, ioctl, pollfd, ppoll, pthread_sigmask, sigaddset, sigemptyset,
    signalfd, signalfd_siginfo, tcgetattr, tcsetattr, termios as Termios,
    time_t, timespec, winsize,
};

use crate::{
    Error, Result,
    raw::{SysEvent, TermSize},
};

static ORIGINAL_TERMINAL_MODE: Mutex<Option<Termios>> = Mutex::new(None);

/// The maximum time you can wait for stdin. Larger values will return error.
///
/// This is unix implementation and thus the maximum is `time_t::MAX` seconds
/// which is `i32::MAX` on 32 bit platforms and `i64::MAX` on 64 bit platforms.
pub const MAX_STDIN_WAIT: Duration = Duration::from_secs(time_t::MAX as u64);

fn get_original_terminal_mode() -> MutexGuard<'static, Option<Termios>> {
    ORIGINAL_TERMINAL_MODE
        .lock()
        .unwrap_or_else(|e| e.into_inner())
}

struct TtyFd {
    fd: RawFd,
    close: bool,
}

impl TtyFd {
    fn get() -> Result<Self> {
        let (fd, close) = if unsafe { libc::isatty(libc::STDIN_FILENO) == 1 } {
            (libc::STDIN_FILENO, false)
        } else {
            (
                fs::OpenOptions::new()
                    .read(true)
                    .write(true)
                    .open("/dev/tty")?
                    .into_raw_fd(),
                true,
            )
        };

        Ok(Self { fd, close })
    }
}

impl AsRawFd for TtyFd {
    fn as_raw_fd(&self) -> RawFd {
        self.fd
    }
}

impl Drop for TtyFd {
    fn drop(&mut self) {
        if self.close {
            _ = unsafe { libc::close(self.fd) };
        }
    }
}

impl From<winsize> for TermSize {
    fn from(value: winsize) -> Self {
        Self {
            char_width: value.ws_col as usize,
            char_height: value.ws_row as usize,
            pixel_width: value.ws_xpixel as usize,
            pixel_height: value.ws_ypixel as usize,
        }
    }
}

/// Check if raw mode on linux is enabled.
pub fn is_raw_mode_enabled() -> bool {
    get_original_terminal_mode().is_some()
}

/// Enable raw mode on linux.
pub(crate) fn enable_raw_mode() -> Result<()> {
    let mut orig_mode = get_original_terminal_mode();

    if orig_mode.is_some() {
        return Ok(());
    }

    let tty = TtyFd::get()?;
    let fd = tty.as_raw_fd();
    let mut ios = get_terminal_attr(fd)?;
    let orig_mode_ios = ios;

    raw_terminal_attr(&mut ios);
    set_terminal_attr(fd, &ios)?;

    *orig_mode = Some(orig_mode_ios);

    Ok(())
}

/// Disable raw mode on linux.
pub(crate) fn disable_raw_mode() -> Result<()> {
    let mut orig_mode = get_original_terminal_mode();

    if let Some(orig_mode_ios) = orig_mode.as_ref() {
        let tty = TtyFd::get()?;
        set_terminal_attr(tty.as_raw_fd(), orig_mode_ios)?;
        *orig_mode = None;
    }

    Ok(())
}

/// Get the window size on linux.
pub(crate) fn window_size() -> Result<TermSize> {
    let tty = TtyFd::get()?;
    let mut size = winsize {
        ws_col: 0,
        ws_row: 0,
        ws_xpixel: 0,
        ws_ypixel: 0,
    };

    Ok(
        to_io_result(unsafe { ioctl(tty.fd, TIOCGWINSZ, &mut size) })
            .map(|_| size.into())?,
    )
}

/// Wait for stdin input on linux with the given timeout. If zero returns
/// immidietly whether there is available input.
pub(crate) fn wait_for_stdin(timeout: Duration) -> Result<bool> {
    let infinite = timeout == Duration::MAX;

    if timeout > MAX_STDIN_WAIT && !infinite {
        return Err(Error::IntConvert);
    }

    let end = if infinite {
        Instant::now()
    } else {
        Instant::now() + timeout
    };

    let mut pdfs = pollfd {
        fd: libc::STDIN_FILENO,
        events: POLLIN,
        revents: 0,
    };

    loop {
        let wait = if infinite {
            MAX_STDIN_WAIT
        } else {
            end.saturating_duration_since(Instant::now())
        };
        let wait = timespec {
            tv_sec: wait.as_secs() as time_t,
            tv_nsec: wait.subsec_nanos().into(),
        };
        let r = unsafe { ppoll(&mut pdfs, 1, &wait, ptr::null()) };

        match r {
            0 => return Ok(false),
            1 => {
                return if (pdfs.revents & POLLERR) == POLLERR {
                    Err(Error::WaitAbandoned)
                } else {
                    Ok(true)
                };
            }
            -1 => {
                let err = io::Error::last_os_error();
                if err.kind() == ErrorKind::Interrupted {
                    continue;
                }
                return Err(err.into());
            }
            _ => return Err(Error::WaitAbandoned),
        }
    }
}

pub(crate) fn init_events() -> Result<()> {
    let res = unsafe {
        let mut signals = mem::zeroed();
        sigemptyset(&mut signals);
        sigaddset(&mut signals, SIGWINCH);
        pthread_sigmask(SIG_BLOCK, &signals, ptr::null_mut())
    };
    to_io_result(res)?;
    Ok(())
}

pub(crate) fn wait_for_event(timeout: Duration) -> Result<SysEvent> {
    let infinite = timeout == Duration::MAX;

    if timeout > MAX_STDIN_WAIT && !infinite {
        return Err(Error::IntConvert);
    }

    let end = if infinite {
        Instant::now()
    } else {
        Instant::now() + timeout
    };

    let signals = unsafe {
        let mut signals = mem::zeroed();
        sigemptyset(&mut signals);
        sigaddset(&mut signals, SIGWINCH);
        signals
    };

    let sigfd = unsafe {
        let fd = signalfd(-1, &signals, 0);
        OwnedFd::from_raw_fd(fd)
    };

    let mut pdfs = [
        pollfd {
            fd: libc::STDIN_FILENO,
            events: POLLIN,
            revents: 0,
        },
        pollfd {
            fd: sigfd.as_raw_fd(),
            events: POLLIN,
            revents: 0,
        },
    ];

    loop {
        let wait = if infinite {
            MAX_STDIN_WAIT
        } else {
            end.saturating_duration_since(Instant::now())
        };
        let wait = timespec {
            tv_sec: wait.as_secs() as time_t,
            tv_nsec: wait.subsec_nanos().into(),
        };
        let r = unsafe {
            ppoll(pdfs.as_mut_ptr(), pdfs.len() as c_ulong, &wait, &signals)
        };

        match r {
            0 => return Ok(SysEvent::Timeout),
            1.. => {
                for e in &pdfs {
                    if e.revents == 0 {
                        continue;
                    }
                    return if (e.revents & POLLERR) == POLLERR {
                        Err(Error::WaitAbandoned)
                    } else {
                        let fd = e.fd;
                        if fd == libc::STDIN_FILENO {
                            Ok(SysEvent::StdinReady)
                        } else if fd == sigfd.as_raw_fd() {
                            let sig = read_signal(&sigfd);
                            if sig == SIGWINCH {
                                Ok(SysEvent::WindowResize)
                            } else {
                                panic!("unhandled signal {sig}.");
                            }
                        } else {
                            panic!("ppoll has event on unknown descriptor.");
                        }
                    };
                }
            }
            -1 => {
                let err = io::Error::last_os_error();
                if err.kind() == ErrorKind::Interrupted {
                    continue;
                }
                return Err(err.into());
            }
            _ => return Err(Error::WaitAbandoned),
        }
    }
}

fn read_signal(fd: &OwnedFd) -> i32 {
    let info = unsafe {
        let mut info: signalfd_siginfo = mem::zeroed();
        libc::read(
            fd.as_raw_fd(),
            ptr::from_mut(&mut info) as *mut c_void,
            mem::size_of::<signalfd_siginfo>(),
        );
        info
    };
    info.ssi_signo as i32
}

fn get_terminal_attr(fd: RawFd) -> Result<Termios> {
    unsafe {
        let mut termios = mem::zeroed();
        to_io_result(tcgetattr(fd, &mut termios))?;
        Ok(termios)
    }
}

fn raw_terminal_attr(termios: &mut Termios) {
    unsafe { cfmakeraw(termios) }
}

fn set_terminal_attr(fd: RawFd, termios: &Termios) -> Result<()> {
    to_io_result(unsafe { tcsetattr(fd, TCSANOW, termios) })?;
    Ok(())
}

fn to_io_result(code: i32) -> io::Result<()> {
    if code == -1 {
        Err(io::Error::last_os_error())
    } else {
        Ok(())
    }
}