#![cfg_attr(target_os = "yggdrasil", feature(yggdrasil_os, rustc_private))] use std::{ fmt, fs::File, io::{self, stdin, stdout, IsTerminal, Read, Stdin, Stdout, Write}, os::fd::{AsRawFd, RawFd} }; pub use self::{input::ReadChar, sys::RawMode}; #[cfg(target_os = "yggdrasil")] mod yggdrasil; #[cfg(target_os = "yggdrasil")] use yggdrasil as sys; #[cfg(unix)] mod unix; #[cfg(unix)] use unix as sys; mod input; pub use input::{InputError, TermKey}; #[derive(thiserror::Error, Debug)] pub enum Error { #[error("I/O error: {0}")] IoError(#[from] io::Error), #[error("Input error: {0}")] InputError(#[from] InputError), } pub trait RawTerminal { fn raw_enter_alternate_mode(&mut self) -> io::Result<()>; fn raw_leave_alternate_mode(&mut self) -> io::Result<()>; fn raw_clear_all(&mut self) -> io::Result<()>; fn raw_clear_line(&mut self, what: u32) -> io::Result<()>; fn raw_move_cursor(&mut self, row: usize, column: usize) -> io::Result<()>; fn raw_set_cursor_style(&mut self, style: CursorStyle) -> io::Result<()>; fn raw_set_color(&mut self, fgbg: u32, color: Color) -> io::Result<()>; fn raw_set_style(&mut self, what: u32) -> io::Result<()>; fn raw_set_cursor_visible(&mut self, visible: bool) -> io::Result<()>; fn raw_size(&self) -> io::Result<(usize, usize)>; } enum TermInput { Stdin(Stdin), File(File) } impl Read for TermInput { fn read(&mut self, buf: &mut [u8]) -> io::Result { match self { Self::Stdin(stdin) => stdin.read(buf), Self::File(file) => file.read(buf) } } } impl AsRawFd for TermInput { fn as_raw_fd(&self) -> RawFd { match self { Self::Stdin(stdin) => stdin.as_raw_fd(), Self::File(file) => file.as_raw_fd(), } } } pub struct Term { stdin: TermInput, stdout: Stdout, raw: RawMode, } #[derive(Debug, Clone, Copy)] pub enum CursorStyle { Default, Line, } #[derive(Debug, Clone, Copy)] #[repr(u32)] pub enum Color { Black = 0, Red = 1, Green = 2, Yellow = 3, Blue = 4, Magenta = 5, Cyan = 6, White = 7, Default = 9, } #[derive(Debug, Clone, Copy)] pub enum Clear { All, LineToEnd, } impl RawTerminal for Stdout { fn raw_enter_alternate_mode(&mut self) -> io::Result<()> { self.write_all(b"\x1B[?1049h") } fn raw_leave_alternate_mode(&mut self) -> io::Result<()> { self.write_all(b"\x1B[?1049l") } fn raw_clear_all(&mut self) -> io::Result<()> { self.write_all(b"\x1B[2J") } fn raw_clear_line(&mut self, what: u32) -> io::Result<()> { write!(self, "\x1B[{}K", what) } fn raw_move_cursor(&mut self, row: usize, column: usize) -> io::Result<()> { write!(self, "\x1B[{};{}f", row + 1, column + 1) } fn raw_set_cursor_style(&mut self, style: CursorStyle) -> io::Result<()> { // TODO yggdrasil support for cursor styles #[cfg(not(target_os = "yggdrasil"))] { match style { CursorStyle::Default => self.write_all(b"\x1B[0 q")?, CursorStyle::Line => self.write_all(b"\x1B[6 q")?, } } #[cfg(target_os = "yggdrasil")] { let _ = style; } Ok(()) } fn raw_set_color(&mut self, fgbg: u32, color: Color) -> io::Result<()> { write!(self, "\x1B[{}{}m", fgbg, color as u32) } fn raw_set_style(&mut self, what: u32) -> io::Result<()> { write!(self, "\x1B[{}m", what) } fn raw_set_cursor_visible(&mut self, visible: bool) -> io::Result<()> { if visible { write!(self, "\x1B[?25h") } else { write!(self, "\x1B[?25l") } } fn raw_size(&self) -> io::Result<(usize, usize)> { unsafe { sys::terminal_size(self) } } } impl Term { pub fn stdin_is_tty() -> bool { stdin().is_terminal() } pub fn input_fd(&self) -> RawFd { self.stdin.as_raw_fd() } pub fn open() -> Result { let stdin = stdin(); let stdin = if stdin.is_terminal() { TermInput::Stdin(stdin) } else { let file = File::open("/dev/tty")?; TermInput::File(file) }; let mut stdout = stdout(); // Set stdin to raw mode let raw = unsafe { RawMode::enter(&stdin)? }; stdout.raw_enter_alternate_mode()?; stdout.raw_clear_all()?; stdout.raw_move_cursor(0, 0)?; Ok(Self { stdin, stdout, raw }) } pub fn set_cursor_position(&mut self, row: usize, column: usize) -> Result<(), Error> { self.stdout .raw_move_cursor(row, column) .map_err(Error::from) } pub fn set_cursor_visible(&mut self, visible: bool) -> Result<(), Error> { #[cfg(unix)] { self.stdout .raw_set_cursor_visible(visible) .map_err(Error::from) } #[cfg(target_os = "yggdrasil")] { let _ = visible; Ok(()) } } pub fn set_cursor_style(&mut self, style: CursorStyle) -> Result<(), Error> { self.stdout.raw_set_cursor_style(style).map_err(Error::from) } pub fn size(&self) -> Result<(usize, usize), Error> { self.stdout.raw_size().map_err(Error::from) } pub fn set_foreground(&mut self, color: Color) -> Result<(), Error> { self.stdout.raw_set_color(3, color).map_err(Error::from) } pub fn set_background(&mut self, color: Color) -> Result<(), Error> { self.stdout.raw_set_color(4, color).map_err(Error::from) } pub fn set_bright(&mut self, bright: bool) -> Result<(), Error> { if bright { self.stdout.raw_set_style(2) } else { self.stdout.raw_set_style(22) } .map_err(Error::from) } pub fn reset_style(&mut self) -> Result<(), Error> { self.stdout.raw_set_style(0).map_err(Error::from) } pub fn clear(&mut self, clear: Clear) -> Result<(), Error> { match clear { Clear::All => self.stdout.raw_clear_all(), Clear::LineToEnd => self.stdout.raw_clear_line(0), } .map_err(Error::from) } pub fn read_key(&mut self) -> Result { let ch = self.stdin.read_char().map_err(Error::from)?; if ch == '\x1B' { return Ok(TermKey::Escape); } Ok(TermKey::Char(ch)) } pub fn flush(&mut self) -> Result<(), Error> { self.stdout.flush().map_err(Error::from) } } impl AsRawFd for Term { fn as_raw_fd(&self) -> RawFd { self.input_fd() } } impl fmt::Write for Term { fn write_str(&mut self, s: &str) -> fmt::Result { self.stdout.write_all(s.as_bytes()).map_err(|_| fmt::Error) } } impl Drop for Term { fn drop(&mut self) { unsafe { self.raw.leave(&self.stdin); } self.stdout.raw_leave_alternate_mode().ok(); } }