termit 0.7.0

Terminal UI over crossterm
Documentation
pub mod tty;

use super::Control;
use crate::{
    input::{async_input, DefaultStdIn},
    prelude::Point,
    Terminal,
};
use log::{trace, warn};
use rustix::fd::AsFd;
use rustix::termios::Termios;
use std::{fs, io, sync::Arc};

pub type DefaultControl = NixTty;
pub type ControlOut = fs::File;
pub type ControlIn = fs::File;

pub(crate) fn terminal() -> io::Result<Terminal<DefaultControl, DefaultStdIn, io::Stdout>> {
    let control = get_tty()?;
    Ok(Terminal::new(control, async_input(), io::stdout()))
}

/// Try to get terminal tty from well known IO handles - stdin, stdout, /dev/tty
pub fn get_tty() -> io::Result<DefaultControl> {
    let input = None
        .or_else(|| tty::is_tty(io::stdin()))
        .and_then(|stdin| stdin.as_fd().try_clone_to_owned().ok())
        .map(fs::File::from)
        .or_else(|| tty::is_tty(tty::open_tty().ok()?))
        .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "no tty was found"))?;

    let output = None
        .or_else(|| tty::is_tty(io::stdout()))
        .and_then(|stdin| stdin.as_fd().try_clone_to_owned().ok())
        .map(fs::File::from)
        .or_else(|| tty::is_tty(tty::open_tty().ok()?))
        .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "no tty was found"))?;

    NixTty::try_from((input, output))
}

/// Wraps a control to reset raw mode on drop
pub struct NixTty {
    reset: Arc<Reset>,
}
impl TryFrom<(fs::File, fs::File)> for NixTty {
    type Error = io::Error;

    fn try_from(value: (fs::File, fs::File)) -> Result<Self, Self::Error> {
        let (input, output) = value;

        let state =
            tty::get_tty_attributes(&input).or_else(|_| tty::get_tty_attributes(&output))?;

        Ok(NixTty {
            reset: Arc::new(Reset {
                state,
                input,
                output,
            }),
        })
    }
}
impl Control for NixTty {
    /// Enables ANSI code support on *nix
    ///
    /// It uses `termios::tcsetattr`
    fn enable_raw_mode(&self) -> std::io::Result<()> {
        tty::enable_raw(&self.reset.input).or_else(|_| tty::enable_raw(&self.reset.output))
    }
    /// Get terminal window size from `termios::tcgetwinsize`
    fn get_size(&self) -> std::io::Result<Point> {
        tty::size_from_fd(&self.reset.output).or_else(|_| tty::size_from_fd(&self.reset.input))
    }
    /// Check if it's a TTY, with `is_terminal`
    fn is_tty(&self) -> bool {
        tty::is_tty(&self.reset.output).is_some() || tty::is_tty(&self.reset.input).is_some()
    }
    fn is_raw(&self) -> bool {
        tty::is_raw(&self.reset.output)
            .or_else(|_| tty::is_raw(&self.reset.input))
            .unwrap_or_default()
    }
    fn is_ansi(&self) -> bool {
        tty::is_raw(&self.reset.output)
            .or_else(|_| tty::is_raw(&self.reset.input))
            .unwrap_or_default()
    }
    fn output(&self) -> &ControlOut {
        &self.reset.output
    }
    fn input(&self) -> &ControlIn {
        &self.reset.input
    }
    fn clone(&self) -> Self {
        Self {
            reset: self.reset.clone(),
        }
    }
}

/// Shared reset object - last one switch the lights off, please
struct Reset {
    state: Termios,
    input: fs::File,
    output: fs::File,
}
impl Drop for Reset {
    fn drop(&mut self) {
        reset(&self.state, &self.input, &self.output)
    }
}
fn reset(tios: &Termios, i: &fs::File, o: &fs::File) {
    trace!("Resetting tty atts for {i:?} and {o:?}");
    tty::set_tty_attributes(i, tios).unwrap_or_else(|e| warn!("could not reset in attrs - {e}"));
    tty::set_tty_attributes(o, tios).unwrap_or_else(|e| warn!("could not reset out attrs - {e}"));
}