diff --git a/Cargo.toml b/Cargo.toml index 2c53c14b..e7383ce3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ keywords = ["event", "color", "cli", "input", "terminal"] exclude = ["target", "Cargo.lock"] readme = "README.md" edition = "2021" -rust-version = "1.58.0" +rust-version = "1.63.0" categories = ["command-line-interface", "command-line-utilities"] [lib] @@ -44,6 +44,7 @@ events = [ "dep:signal-hook-mio", ] # Enables reading input/events from the system. serde = ["dep:serde", "bitflags/serde"] # Enables 'serde' for various types. +rustix = ["dep:rustix"] # On Unix, enables using rustix as the underlying interface. # # Shared dependencies @@ -71,13 +72,14 @@ crossterm_winapi = { version = "0.9.1", optional = true } # UNIX dependencies # [target.'cfg(unix)'.dependencies] -libc = "0.2" signal-hook = { version = "0.3.17", optional = true } filedescriptor = { version = "0.8", optional = true } +libc = { version = "0.2", default-features = false } mio = { version = "0.8", features = ["os-poll"], optional = true } signal-hook-mio = { version = "0.2.3", features = [ "support-v0_8", ], optional = true } +rustix = { version = "0.38.0", default-features = false, features = ["std", "stdio", "termios"], optional = true } # # Dev dependencies (examples, ...) diff --git a/src/event.rs b/src/event.rs index 2a620e12..7c2de1c5 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1150,7 +1150,6 @@ pub(crate) enum InternalEvent { #[cfg(test)] mod tests { use std::collections::hash_map::DefaultHasher; - use std::hash::{Hash, Hasher}; use super::*; use KeyCode::*; diff --git a/src/event/sys/unix/parse.rs b/src/event/sys/unix/parse.rs index 2019b5f2..81e9b800 100644 --- a/src/event/sys/unix/parse.rs +++ b/src/event/sys/unix/parse.rs @@ -863,8 +863,6 @@ pub(crate) fn parse_utf8_char(buffer: &[u8]) -> io::Result> { #[cfg(test)] mod tests { - use crate::event::{KeyEventState, KeyModifiers, MouseButton, MouseEvent}; - use super::*; #[test] diff --git a/src/lib.rs b/src/lib.rs index 0217bef1..f9600efc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -249,6 +249,8 @@ pub mod tty; pub mod ansi_support; mod command; pub(crate) mod macros; +#[cfg(unix)] +mod sys; #[cfg(all(windows, not(feature = "windows")))] compile_error!("Compiling on Windows with \"windows\" feature disabled. Feature \"windows\" should only be disabled when project will never be compiled on Windows."); diff --git a/src/style/types/color.rs b/src/style/types/color.rs index d4d52844..02544ac4 100644 --- a/src/style/types/color.rs +++ b/src/style/types/color.rs @@ -1,7 +1,4 @@ -use std::{ - convert::{AsRef, TryFrom}, - str::FromStr, -}; +use std::str::FromStr; #[cfg(feature = "serde")] use std::fmt; diff --git a/src/sys.rs b/src/sys.rs new file mode 100644 index 00000000..b131397c --- /dev/null +++ b/src/sys.rs @@ -0,0 +1,12 @@ +//! Interface to the C system library. +//! +//! This uses either rustix or libc. + +#[cfg(feature = "rustix")] +pub(crate) use rustix::*; + +#[cfg(not(feature = "rustix"))] +mod minirustix; + +#[cfg(not(feature = "rustix"))] +pub(crate) use minirustix::*; diff --git a/src/sys/minirustix.rs b/src/sys/minirustix.rs new file mode 100644 index 00000000..86d7fea6 --- /dev/null +++ b/src/sys/minirustix.rs @@ -0,0 +1,94 @@ +//! Emulates rustix's interface using libc. + +pub(crate) mod io { + use super::cvt; + use std::io::Result; + use std::os::unix::io::{AsFd, AsRawFd}; + + pub(crate) fn read(f: impl AsFd, buf: &mut [u8]) -> Result { + unsafe { + cvt(libc::read( + f.as_fd().as_raw_fd(), + buf.as_mut_ptr().cast(), + buf.len() as _, + ) as i32) + .map(|x| x as usize) + } + } +} + +pub(crate) mod stdio { + use std::os::unix::io::BorrowedFd; + + pub(crate) fn stdin() -> BorrowedFd<'static> { + unsafe { BorrowedFd::borrow_raw(libc::STDIN_FILENO) } + } + + pub(crate) fn stdout() -> BorrowedFd<'static> { + unsafe { BorrowedFd::borrow_raw(libc::STDOUT_FILENO) } + } +} + +pub(crate) mod termios { + use super::cvt; + use std::io::Result; + use std::mem::MaybeUninit; + use std::os::unix::io::{AsFd, AsRawFd, BorrowedFd}; + + pub(crate) type Termios = libc::termios; + pub(crate) type Winsize = libc::winsize; + + #[repr(u32)] + pub(crate) enum OptionalActions { + Now = 0, + // All others are unused by crossterm. + } + + pub(crate) fn isatty(fd: BorrowedFd<'_>) -> bool { + unsafe { libc::isatty(fd.as_raw_fd()) != 0 } + } + + pub(crate) fn tcgetwinsize(fd: impl AsFd) -> Result { + unsafe { + let mut buf = MaybeUninit::::uninit(); + cvt(libc::ioctl( + fd.as_fd().as_raw_fd(), + libc::TIOCGWINSZ, + buf.as_mut_ptr(), + ))?; + Ok(buf.assume_init()) + } + } + + pub(crate) fn tcgetattr(fd: impl AsFd) -> Result { + unsafe { + let mut buf = MaybeUninit::::uninit(); + cvt(libc::tcgetattr(fd.as_fd().as_raw_fd(), buf.as_mut_ptr()))?; + Ok(buf.assume_init()) + } + } + + pub(crate) fn tcsetattr( + fd: impl AsFd, + optional: OptionalActions, + termios: &Termios, + ) -> Result<()> { + unsafe { + cvt(libc::tcsetattr( + fd.as_fd().as_raw_fd(), + optional as _, + termios, + ))?; + + Ok(()) + } + } +} + +fn cvt(res: i32) -> std::io::Result { + if res < 0 { + Err(std::io::Error::last_os_error()) + } else { + Ok(res) + } +} diff --git a/src/terminal/sys/file_descriptor.rs b/src/terminal/sys/file_descriptor.rs index 81c3fb2e..83ecf5c5 100644 --- a/src/terminal/sys/file_descriptor.rs +++ b/src/terminal/sys/file_descriptor.rs @@ -1,65 +1,42 @@ -use std::{ - fs, io, - os::unix::{ - io::{IntoRawFd, RawFd}, - prelude::AsRawFd, - }, -}; - -use libc::size_t; +use crate::sys; +use std::os::unix::io::{AsFd, AsRawFd, BorrowedFd, OwnedFd, RawFd}; +use std::{fs, io}; /// A file descriptor wrapper. /// /// It allows to retrieve raw file descriptor, write to the file descriptor and /// mainly it closes the file descriptor once dropped. #[derive(Debug)] -pub struct FileDesc { - fd: RawFd, - close_on_drop: bool, +pub enum FileDesc { + Owned(OwnedFd), + Static(BorrowedFd<'static>), } impl FileDesc { - /// Constructs a new `FileDesc` with the given `RawFd`. - /// - /// # Arguments - /// - /// * `fd` - raw file descriptor - /// * `close_on_drop` - specify if the raw file descriptor should be closed once the `FileDesc` is dropped - pub fn new(fd: RawFd, close_on_drop: bool) -> FileDesc { - FileDesc { fd, close_on_drop } - } - pub fn read(&self, buffer: &mut [u8]) -> io::Result { - let result = unsafe { - libc::read( - self.fd, - buffer.as_mut_ptr() as *mut libc::c_void, - buffer.len() as size_t, - ) + let fd = match self { + Self::Owned(fd) => fd.as_fd(), + Self::Static(fd) => fd.as_fd(), }; - if result < 0 { - Err(io::Error::last_os_error()) - } else { - Ok(result as usize) - } + let result = sys::io::read(fd, buffer)?; + Ok(result) } /// Returns the underlying file descriptor. pub fn raw_fd(&self) -> RawFd { - self.fd + match self { + Self::Owned(fd) => fd.as_raw_fd(), + Self::Static(fd) => fd.as_raw_fd(), + } } } -impl Drop for FileDesc { - fn drop(&mut self) { - if self.close_on_drop { - // Note that errors are ignored when closing a file descriptor. The - // reason for this is that if an error occurs we don't actually know if - // the file descriptor was closed or not, and if we retried (for - // something like EINTR), we might close another valid file descriptor - // opened after we closed ours. - let _ = unsafe { libc::close(self.fd) }; +impl AsFd for FileDesc { + fn as_fd(&self) -> BorrowedFd<'_> { + match self { + Self::Owned(fd) => fd.as_fd(), + Self::Static(fd) => fd.as_fd(), } } } @@ -72,18 +49,15 @@ impl AsRawFd for FileDesc { /// Creates a file descriptor pointing to the standard input or `/dev/tty`. pub fn tty_fd() -> io::Result { - let (fd, close_on_drop) = if unsafe { libc::isatty(libc::STDIN_FILENO) == 1 } { - (libc::STDIN_FILENO, false) + if sys::termios::isatty(sys::stdio::stdin()) { + Ok(FileDesc::Static(sys::stdio::stdin())) } else { - ( + Ok(FileDesc::Owned( fs::OpenOptions::new() .read(true) .write(true) .open("/dev/tty")? - .into_raw_fd(), - true, - ) - }; - - Ok(FileDesc::new(fd, close_on_drop)) + .into(), + )) + } } diff --git a/src/terminal/sys/unix.rs b/src/terminal/sys/unix.rs index ed545c5b..66e7adf4 100644 --- a/src/terminal/sys/unix.rs +++ b/src/terminal/sys/unix.rs @@ -1,19 +1,18 @@ //! UNIX related logic for terminal manipulation. +use crate::sys::{ + self, + termios::{Termios, Winsize}, +}; use crate::terminal::{ sys::file_descriptor::{tty_fd, FileDesc}, WindowSize, }; -use libc::{ - cfmakeraw, ioctl, tcgetattr, tcsetattr, termios as Termios, winsize, STDOUT_FILENO, TCSANOW, - TIOCGWINSZ, -}; use parking_lot::Mutex; use std::fs::File; +use std::os::unix::io::AsFd; -use std::os::unix::io::{IntoRawFd, RawFd}; - -use std::{io, mem, process}; +use std::{io, process}; // Some(Termios) -> we're in the raw mode and this is the previous mode // None -> we're not in the raw mode @@ -23,8 +22,8 @@ pub(crate) fn is_raw_mode_enabled() -> bool { TERMINAL_MODE_PRIOR_RAW_MODE.lock().is_some() } -impl From for WindowSize { - fn from(size: winsize) -> WindowSize { +impl From for WindowSize { + fn from(size: Winsize) -> WindowSize { WindowSize { columns: size.ws_col, rows: size.ws_row, @@ -36,27 +35,16 @@ impl From for WindowSize { #[allow(clippy::useless_conversion)] pub(crate) fn window_size() -> io::Result { - // http://rosettacode.org/wiki/Terminal_control/Dimensions#Library:_BSD_libc - let mut size = winsize { - ws_row: 0, - ws_col: 0, - ws_xpixel: 0, - ws_ypixel: 0, - }; - - let file = File::open("/dev/tty").map(|file| (FileDesc::new(file.into_raw_fd(), true))); + let file = File::open("/dev/tty").map(|file| FileDesc::Owned(file.into())); let fd = if let Ok(file) = &file { - file.raw_fd() + file.as_fd() } else { // Fallback to libc::STDOUT_FILENO if /dev/tty is missing - STDOUT_FILENO + sys::stdio::stdout() }; - if wrap_with_result(unsafe { ioctl(fd, TIOCGWINSZ.into(), &mut size) }).is_ok() { - return Ok(size.into()); - } - - Err(std::io::Error::last_os_error().into()) + let size = sys::termios::tcgetwinsize(fd)?; + Ok(size.into()) } #[allow(clippy::useless_conversion)] @@ -76,12 +64,17 @@ pub(crate) fn enable_raw_mode() -> io::Result<()> { } let tty = tty_fd()?; - let fd = tty.raw_fd(); - let mut ios = get_terminal_attr(fd)?; - let original_mode_ios = ios; + let mut ios = get_terminal_attr(&tty)?; + let original_mode_ios = ios.clone(); + + #[cfg(feature = "rustix")] + ios.make_raw(); + #[cfg(not(feature = "rustix"))] + unsafe { + libc::cfmakeraw(&mut ios); + } - raw_terminal_attr(&mut ios); - set_terminal_attr(fd, &ios)?; + set_terminal_attr(&tty, &ios)?; // Keep it last - set the original mode only if we were able to switch to the raw mode *original_mode = Some(original_mode_ios); @@ -99,7 +92,7 @@ pub(crate) fn disable_raw_mode() -> io::Result<()> { if let Some(original_mode_ios) = original_mode.as_ref() { let tty = tty_fd()?; - set_terminal_attr(tty.raw_fd(), original_mode_ios)?; + set_terminal_attr(&tty, original_mode_ios)?; // Keep it last - remove the original mode only if we were able to switch back *original_mode = None; } @@ -214,27 +207,12 @@ fn tput_size() -> Option<(u16, u16)> { } } -// Transform the given mode into an raw mode (non-canonical) mode. -fn raw_terminal_attr(termios: &mut Termios) { - unsafe { cfmakeraw(termios) } +fn get_terminal_attr(fd: impl AsFd) -> io::Result { + let result = sys::termios::tcgetattr(fd)?; + Ok(result) } -fn get_terminal_attr(fd: RawFd) -> io::Result { - unsafe { - let mut termios = mem::zeroed(); - wrap_with_result(tcgetattr(fd, &mut termios))?; - Ok(termios) - } -} - -fn set_terminal_attr(fd: RawFd, termios: &Termios) -> io::Result<()> { - wrap_with_result(unsafe { tcsetattr(fd, TCSANOW, termios) }) -} - -fn wrap_with_result(result: i32) -> io::Result<()> { - if result == -1 { - Err(io::Error::last_os_error()) - } else { - Ok(()) - } +fn set_terminal_attr(fd: impl AsFd, termios: &Termios) -> io::Result<()> { + sys::termios::tcsetattr(fd, sys::termios::OptionalActions::Now, termios)?; + Ok(()) } diff --git a/src/tty.rs b/src/tty.rs index 78e32aae..e194a32d 100644 --- a/src/tty.rs +++ b/src/tty.rs @@ -30,7 +30,7 @@ pub trait IsTty { impl IsTty for S { fn is_tty(&self) -> bool { let fd = self.as_raw_fd(); - unsafe { libc::isatty(fd) == 1 } + crate::sys::termios::isatty(unsafe { std::os::unix::io::BorrowedFd::borrow_raw(fd) }) } }