term_manager/
lib.rs

1// Copyright (c) 2025 Sebastian Ibanez
2// Author: Sebastian Ibanez
3// Created: 2025-09-14
4
5use std::{
6    fmt::Display,
7    io::{self, Read, Stdin, Stdout, Write},
8    os::fd::{AsRawFd, RawFd},
9    u8,
10};
11
12/// Error type for IO and UNIX errors.
13#[derive(Debug)]
14pub enum Error {
15    Io(io::Error),
16    Errno(nix::errno::Errno),
17}
18
19impl From<io::Error> for Error {
20    fn from(e: io::Error) -> Self {
21        Error::Io(e)
22    }
23}
24
25impl From<nix::errno::Errno> for Error {
26    fn from(e: nix::errno::Errno) -> Self {
27        Error::Errno(e)
28    }
29}
30
31impl Display for Error {
32    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33        match self {
34            Error::Io(e) => write!(f, "IO error: {}", e),
35            Error::Errno(e) => write!(f, "UNIX error: {}", e),
36        }
37    }
38}
39
40/// Manipulates terminal state via libc.
41pub struct TermManager {
42    stdin: Stdin,
43    stdout: Stdout,
44    fd: RawFd,
45    original_termios: libc::termios,
46}
47
48impl TermManager {
49    /// Create a new TermManager in raw mode.
50    pub fn new() -> Result<TermManager, Error> {
51        let stdin = io::stdin();
52        let stdout = io::stdout();
53        let fd = stdin.as_raw_fd();
54        let original_termios = enable_raw_mode(fd)?;
55
56        Ok(TermManager {
57            stdin,
58            stdout,
59            fd,
60            original_termios,
61        })
62    }
63
64    pub fn get_stdin(&self) -> &Stdin {
65        &self.stdin
66    }
67
68    pub fn get_stout(&self) -> &Stdout {
69        &self.stdout
70    }
71
72    /// Flush stdout.
73    pub fn flush(&mut self) -> Result<(), Error> {
74        match self.stdout.flush() {
75            Ok(_) => Ok(()),
76            Err(e) => Err(Error::Io(e)),
77        }
78    }
79
80    /// Write buffer to stdout.
81    pub fn write(&mut self, data: &[u8]) -> Result<(), Error> {
82        match self.stdout.write(data) {
83            Ok(r) => {
84                if r <= 0 && data.len() <= 0 {
85                    let msg = format!("only wrote {} of {} bytes", r, data.len());
86                    return Err(Error::Io(io::Error::new(io::ErrorKind::WriteZero, msg)));
87                }
88
89                Ok(())
90            }
91            Err(e) => Err(Error::Io(e)),
92        }
93    }
94
95    /// Read byte from stdin. Return io::ErrorKind::WriteZero if no byte read.
96    pub fn read(&mut self, buf: &mut [u8; 1]) -> Result<usize, Error> {
97        match self.stdin.read(buf) {
98            Ok(0) => {
99                return Err(Error::Io(io::Error::new(
100                    io::ErrorKind::WriteZero,
101                    "read 0 bytes from stdin",
102                )));
103            }
104            Ok(bytes_read) => Ok(bytes_read),
105            Err(e) => Err(Error::Io(e)),
106        }
107    }
108}
109
110impl Drop for TermManager {
111    fn drop(&mut self) {
112        disable_raw_mode(self.fd, self.original_termios).unwrap();
113    }
114}
115
116/// Enable raw mode by disabling canonical mode and echo.
117fn enable_raw_mode(fd: RawFd) -> Result<libc::termios, Error> {
118    let original_termios = get_termios(fd)?;
119    let mut raw = original_termios;
120    raw.c_lflag &= !(libc::ICANON | libc::ECHO);
121    raw.c_cc[libc::VMIN] = 1;
122    raw.c_cc[libc::VTIME] = 0;
123    set_termios(fd, &raw)?;
124    Ok(original_termios)
125}
126
127/// Disable raw mode and reset terminal interface.
128fn disable_raw_mode(fd: RawFd, original_termios: libc::termios) -> Result<(), Error> {
129    set_termios(fd, &original_termios)?;
130    Ok(())
131}
132
133/// Get termios from raw file descriptors.
134fn get_termios(fd: RawFd) -> Result<libc::termios, Error> {
135    let mut termios = std::mem::MaybeUninit::uninit();
136    let res = unsafe { libc::tcgetattr(fd, termios.as_mut_ptr()) };
137    if res != 0 {
138        return Err(Error::Io(io::Error::last_os_error()));
139    }
140    Ok(unsafe { termios.assume_init() })
141}
142
143/// Set termios settings from raw file descriptors.
144fn set_termios(fd: RawFd, termios: &libc::termios) -> Result<(), Error> {
145    let res = unsafe { libc::tcsetattr(fd, libc::TCSANOW, termios) };
146    if res != 0 {
147        return Err(Error::Io(io::Error::last_os_error()));
148    }
149    Ok(())
150}