taba-cli 0.1.1

The Taba client command line.
Documentation
//! The real terminal, in raw mode and with no library: `stty` on Linux and macOS, the console's
//! `SetConsoleMode` on Windows (with virtual sequences, Windows 10 and later).

use std::collections::VecDeque;
use std::io::{self, Read, Write};

use super::keys::{self, Key};
use super::{Frame, Terminal};

pub struct RawTerminal {
    pending: VecDeque<Key>,
    color: bool,
    saved: sys::Saved,
}

impl RawTerminal {
    /// Puts the terminal in raw mode and enters the alternate screen. Fails if the input or output are not
    /// a terminal.
    pub fn open() -> io::Result<RawTerminal> {
        let saved = sys::enter_raw()?;
        let mut out = io::stdout();
        out.write_all(b"\x1b[?1049h\x1b[?25l")?;
        out.flush()?;
        Ok(RawTerminal {
            pending: VecDeque::new(),
            color: std::env::var_os("NO_COLOR").is_none_or(|v| v.is_empty()),
            saved,
        })
    }
}

impl Drop for RawTerminal {
    fn drop(&mut self) {
        let mut out = io::stdout();
        let _ = out.write_all(b"\x1b[0m\x1b[?25h\x1b[?1049l");
        let _ = out.flush();
        sys::restore(&self.saved);
    }
}

impl Terminal for RawTerminal {
    fn size(&self) -> (usize, usize) {
        sys::size().unwrap_or((80, 24))
    }

    fn read_key(&mut self) -> io::Result<Option<Key>> {
        loop {
            if let Some(key) = self.pending.pop_front() {
                return Ok(Some(key));
            }
            let mut buf = [0u8; 256];
            let n = io::stdin().lock().read(&mut buf)?;
            if n == 0 {
                return Ok(None);
            }
            self.pending.extend(keys::decode(&buf[..n]));
        }
    }

    fn draw(&mut self, frame: &Frame) -> io::Result<()> {
        let mut out = String::new();
        out.push_str("\x1b[?25l");
        for (row, line) in frame.lines.iter().enumerate() {
            out.push_str(&format!("\x1b[{};1H{line}\x1b[0m\x1b[K", row + 1));
        }
        out.push_str(&format!("\x1b[{};1H\x1b[J", frame.lines.len() + 1));
        if let Some((row, col)) = frame.cursor {
            out.push_str(&format!("\x1b[{};{}H\x1b[?25h", row + 1, col + 1));
        }
        let mut stdout = io::stdout();
        stdout.write_all(out.as_bytes())?;
        stdout.flush()
    }

    fn color(&self) -> bool {
        self.color
    }
}

#[cfg(unix)]
mod sys {
    use std::io;
    use std::process::{Command, Stdio};

    pub struct Saved(String);

    fn stty(args: &[&str]) -> io::Result<String> {
        let out = Command::new("stty")
            .args(args)
            .stdin(Stdio::inherit())
            .stderr(Stdio::null())
            .output()?;
        if !out.status.success() {
            return Err(io::Error::other("stty failed (is the input a terminal?)"));
        }
        Ok(String::from_utf8_lossy(&out.stdout).trim().to_owned())
    }

    pub fn enter_raw() -> io::Result<Saved> {
        let saved = stty(&["-g"])?;
        stty(&["raw", "-echo"])?;
        Ok(Saved(saved))
    }

    pub fn restore(saved: &Saved) {
        let _ = stty(&[&saved.0]);
    }

    pub fn size() -> Option<(usize, usize)> {
        let text = stty(&["size"]).ok()?;
        let mut parts = text.split_whitespace();
        let rows: usize = parts.next()?.parse().ok()?;
        let cols: usize = parts.next()?.parse().ok()?;
        Some((cols, rows))
    }
}

#[cfg(windows)]
mod sys {
    use std::io;

    type Handle = isize;

    #[repr(C)]
    #[derive(Default, Clone, Copy)]
    struct Coord {
        x: i16,
        y: i16,
    }

    #[repr(C)]
    #[derive(Default, Clone, Copy)]
    struct SmallRect {
        left: i16,
        top: i16,
        right: i16,
        bottom: i16,
    }

    #[repr(C)]
    #[derive(Default, Clone, Copy)]
    struct ScreenBufferInfo {
        size: Coord,
        cursor: Coord,
        attributes: u16,
        window: SmallRect,
        maximum: Coord,
    }

    #[link(name = "kernel32")]
    extern "system" {
        fn GetStdHandle(which: u32) -> Handle;
        fn GetConsoleMode(handle: Handle, mode: *mut u32) -> i32;
        fn SetConsoleMode(handle: Handle, mode: u32) -> i32;
        fn GetConsoleScreenBufferInfo(handle: Handle, info: *mut ScreenBufferInfo) -> i32;
        fn GetConsoleOutputCP() -> u32;
        fn SetConsoleOutputCP(code_page: u32) -> i32;
    }

    const STD_INPUT: u32 = -10i32 as u32;
    const STD_OUTPUT: u32 = -11i32 as u32;
    const ENABLE_PROCESSED_INPUT: u32 = 0x0001;
    const ENABLE_LINE_INPUT: u32 = 0x0002;
    const ENABLE_ECHO_INPUT: u32 = 0x0004;
    const ENABLE_VIRTUAL_TERMINAL_INPUT: u32 = 0x0200;
    const ENABLE_PROCESSED_OUTPUT: u32 = 0x0001;
    const ENABLE_VIRTUAL_TERMINAL_PROCESSING: u32 = 0x0004;
    const UTF8: u32 = 65001;

    pub struct Saved {
        input: u32,
        output: u32,
        code_page: u32,
    }

    fn mode(handle: Handle) -> io::Result<u32> {
        let mut mode = 0;
        // SAFETY: `handle` comes from `GetStdHandle` and `mode` is a `u32` valid for writing.
        if unsafe { GetConsoleMode(handle, &mut mode) } == 0 {
            return Err(io::Error::last_os_error());
        }
        Ok(mode)
    }

    fn set_mode(handle: Handle, mode: u32) -> io::Result<()> {
        // SAFETY: only passes the value to the console.
        if unsafe { SetConsoleMode(handle, mode) } == 0 {
            return Err(io::Error::last_os_error());
        }
        Ok(())
    }

    pub fn enter_raw() -> io::Result<Saved> {
        // SAFETY: `GetStdHandle` and `GetConsoleOutputCP` only read process state.
        let (input, output, code_page) = unsafe {
            (
                GetStdHandle(STD_INPUT),
                GetStdHandle(STD_OUTPUT),
                GetConsoleOutputCP(),
            )
        };
        let saved = Saved {
            input: mode(input)?,
            output: mode(output)?,
            code_page,
        };
        set_mode(
            input,
            (saved.input & !(ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT))
                | ENABLE_VIRTUAL_TERMINAL_INPUT,
        )?;
        if let Err(e) = set_mode(
            output,
            saved.output | ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING,
        ) {
            let _ = set_mode(input, saved.input);
            return Err(e);
        }
        // SAFETY: only changes the output code page, restored in `restore`.
        unsafe { SetConsoleOutputCP(UTF8) };
        Ok(saved)
    }

    pub fn restore(saved: &Saved) {
        // SAFETY: gives the console back the values it had.
        unsafe {
            let _ = SetConsoleMode(GetStdHandle(STD_INPUT), saved.input);
            let _ = SetConsoleMode(GetStdHandle(STD_OUTPUT), saved.output);
            SetConsoleOutputCP(saved.code_page);
        }
    }

    pub fn size() -> Option<(usize, usize)> {
        let mut info = ScreenBufferInfo::default();
        // SAFETY: `info` is a structure valid for writing, in the format the console expects.
        let ok = unsafe { GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT), &mut info) };
        if ok == 0 {
            return None;
        }
        let w = (info.window.right - info.window.left + 1) as usize;
        let h = (info.window.bottom - info.window.top + 1) as usize;
        Some((w, h))
    }
}

#[cfg(not(any(unix, windows)))]
mod sys {
    use std::io;

    pub struct Saved;

    pub fn enter_raw() -> io::Result<Saved> {
        Err(io::Error::new(
            io::ErrorKind::Unsupported,
            "this system does not have the interactive screen",
        ))
    }

    pub fn restore(_: &Saved) {}

    pub fn size() -> Option<(usize, usize)> {
        None
    }
}