terminal-utils 0.1.0

A collection of utilities for working with the terminal.
Documentation
use std::fmt::Debug;
use std::fs::File;
use std::os::fd::{AsRawFd, RawFd};
use std::{io, mem};

use crate::TerminalSize;

#[derive(Clone, Copy)]
pub struct TerminalState(libc::termios);

impl Debug for TerminalState {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_tuple("TerminalState")
            .field(&self.0.c_iflag)
            .field(&self.0.c_oflag)
            .field(&self.0.c_cflag)
            .field(&self.0.c_lflag)
            .field(&self.0.c_cc)
            .field(&self.0.c_ispeed)
            .field(&self.0.c_ospeed)
            .finish()
    }
}

pub fn size() -> Result<TerminalSize, io::Error> {
    let tty = get_tty()?;
    let fd = tty.as_raw_fd();

    let info = get_winsize(fd)?;

    Ok(TerminalSize {
        width: info.ws_col,
        height: info.ws_row,

        pixel_width: info.ws_xpixel,
        pixel_height: info.ws_ypixel,
    })
}

pub fn is_raw_mode_enabled() -> Result<bool, io::Error> {
    let tty = get_tty()?;
    let fd = tty.as_raw_fd();

    let termios = get_terminal_attr(fd)?;
    Ok((termios.c_lflag & libc::ICANON) == 0)
}

pub fn enable_raw_mode() -> Result<TerminalState, io::Error> {
    let tty = get_tty()?;
    let fd = tty.as_raw_fd();

    let mut termios = get_terminal_attr(fd)?;
    let original_termios = termios;

    unsafe { libc::cfmakeraw(&mut termios) };
    set_terminal_attr(fd, &termios)?;

    Ok(TerminalState(original_termios))
}

pub fn restore_mode(original_termios: TerminalState) -> Result<(), io::Error> {
    let tty = get_tty()?;
    let fd = tty.as_raw_fd();

    set_terminal_attr(fd, &original_termios.0)?;

    Ok(())
}

#[cfg(feature = "tokio")]
pub fn spawn_on_resize_task(
    tx: tokio::sync::watch::Sender<TerminalSize>,
) -> Result<tokio::task::JoinHandle<()>, io::Error> {
    let mut signal = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::window_change())?;

    let task = tokio::spawn(async move {
        loop {
            signal.recv().await;

            if let Ok(size) = size() {
                tx.send_replace(size);
            }
        }
    });
    Ok(task)
}

fn get_tty() -> Result<File, io::Error> {
    File::open("/dev/tty")
}

fn get_winsize(fd: RawFd) -> Result<libc::winsize, io::Error> {
    let mut info: libc::winsize = unsafe { mem::zeroed() };
    wrap_error(unsafe { libc::ioctl(fd, libc::TIOCGWINSZ, &mut info) })?;

    Ok(info)
}

fn get_terminal_attr(fd: RawFd) -> Result<libc::termios, io::Error> {
    let mut termios: libc::termios = unsafe { mem::zeroed() };
    wrap_error(unsafe { libc::tcgetattr(fd, &mut termios) })?;

    Ok(termios)
}

fn set_terminal_attr(fd: RawFd, termios: &libc::termios) -> Result<(), io::Error> {
    wrap_error(unsafe { libc::tcsetattr(fd, libc::TCSANOW, termios) })?;

    Ok(())
}

fn wrap_error(result: libc::c_int) -> io::Result<()> {
    if result == -1 {
        Err(io::Error::last_os_error())
    } else {
        Ok(())
    }
}