diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c7f661c..41e438b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +# Unreleased + +- Use Rustix by default instead of libc. Libc can be re-enabled if necessary with the libc feature flag. +- `FileDesc` now requires a lifetime annotation. + # Version 0.27.1 ## Added ⭐ diff --git a/Cargo.toml b/Cargo.toml index 2c53c14b..cee16975 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] @@ -71,7 +71,14 @@ crossterm_winapi = { version = "0.9.1", optional = true } # UNIX dependencies # [target.'cfg(unix)'.dependencies] -libc = "0.2" +# Default to using rustix for UNIX systems, but provide an option to use libc for backwards +# compatibility. +libc = { version = "0.2", default-features = false, optional = true } +rustix = { version = "0.38.34", default-features = false, features = [ + "std", + "stdio", + "termios", +] } signal-hook = { version = "0.3.17", optional = true } filedescriptor = { version = "0.8", optional = true } mio = { version = "0.8", features = ["os-poll"], optional = true } diff --git a/src/event/source/unix/mio.rs b/src/event/source/unix/mio.rs index 8372a1dd..0368e61b 100644 --- a/src/event/source/unix/mio.rs +++ b/src/event/source/unix/mio.rs @@ -26,7 +26,7 @@ pub(crate) struct UnixInternalEventSource { events: Events, parser: Parser, tty_buffer: [u8; TTY_BUFFER_SIZE], - tty_fd: FileDesc, + tty_fd: FileDesc<'static>, signals: Signals, #[cfg(feature = "event-stream")] waker: Waker, @@ -37,7 +37,7 @@ impl UnixInternalEventSource { UnixInternalEventSource::from_file_descriptor(tty_fd()?) } - pub(crate) fn from_file_descriptor(input_fd: FileDesc) -> io::Result { + pub(crate) fn from_file_descriptor(input_fd: FileDesc<'static>) -> io::Result { let poll = Poll::new()?; let registry = poll.registry(); diff --git a/src/event/source/unix/tty.rs b/src/event/source/unix/tty.rs index b8cbfeff..03d76b40 100644 --- a/src/event/source/unix/tty.rs +++ b/src/event/source/unix/tty.rs @@ -1,6 +1,10 @@ +#[cfg(feature = "libc")] use std::os::unix::prelude::AsRawFd; use std::{collections::VecDeque, io, os::unix::net::UnixStream, time::Duration}; +#[cfg(not(feature = "libc"))] +use rustix::fd::{AsFd, AsRawFd}; + use signal_hook::low_level::pipe; use crate::event::timeout::PollTimeout; @@ -38,7 +42,7 @@ const TTY_BUFFER_SIZE: usize = 1_024; pub(crate) struct UnixInternalEventSource { parser: Parser, tty_buffer: [u8; TTY_BUFFER_SIZE], - tty: FileDesc, + tty: FileDesc<'static>, winch_signal_receiver: UnixStream, #[cfg(feature = "event-stream")] wake_pipe: WakePipe, @@ -56,7 +60,7 @@ impl UnixInternalEventSource { UnixInternalEventSource::from_file_descriptor(tty_fd()?) } - pub(crate) fn from_file_descriptor(input_fd: FileDesc) -> io::Result { + pub(crate) fn from_file_descriptor(input_fd: FileDesc<'static>) -> io::Result { Ok(UnixInternalEventSource { parser: Parser::default(), tty_buffer: [0u8; TTY_BUFFER_SIZE], @@ -64,7 +68,10 @@ impl UnixInternalEventSource { winch_signal_receiver: { let (receiver, sender) = nonblocking_unix_pair()?; // Unregistering is unnecessary because EventSource is a singleton + #[cfg(feature = "libc")] pipe::register(libc::SIGWINCH, sender)?; + #[cfg(not(feature = "libc"))] + pipe::register(rustix::process::Signal::Winch as i32, sender)?; receiver }, #[cfg(feature = "event-stream")] @@ -157,7 +164,10 @@ impl EventSource for UnixInternalEventSource { } } if fds[1].revents & POLLIN != 0 { + #[cfg(feature = "libc")] let fd = FileDesc::new(self.winch_signal_receiver.as_raw_fd(), false); + #[cfg(not(feature = "libc"))] + let fd = FileDesc::Borrowed(self.winch_signal_receiver.as_fd()); // drain the pipe while read_complete(&fd, &mut [0; 1024])? != 0 {} // TODO Should we remove tput? diff --git a/src/terminal/sys/file_descriptor.rs b/src/terminal/sys/file_descriptor.rs index 81c3fb2e..baff266c 100644 --- a/src/terminal/sys/file_descriptor.rs +++ b/src/terminal/sys/file_descriptor.rs @@ -1,32 +1,51 @@ +use std::io; + +#[cfg(feature = "libc")] +use libc::size_t; +#[cfg(not(feature = "libc"))] +use rustix::fd::{AsFd, AsRawFd, BorrowedFd, OwnedFd, RawFd}; +#[cfg(feature = "libc")] use std::{ - fs, io, + fs, + marker::PhantomData, os::unix::{ io::{IntoRawFd, RawFd}, prelude::AsRawFd, }, }; -use libc::size_t; - /// 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 { +#[cfg(feature = "libc")] +pub struct FileDesc<'a> { fd: RawFd, close_on_drop: bool, + phantom: PhantomData<&'a ()>, } -impl FileDesc { +#[cfg(not(feature = "libc"))] +pub enum FileDesc<'a> { + Owned(OwnedFd), + Borrowed(BorrowedFd<'a>), +} + +#[cfg(feature = "libc")] +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 new(fd: RawFd, close_on_drop: bool) -> FileDesc<'static> { + FileDesc { + fd, + close_on_drop, + phantom: PhantomData, + } } pub fn read(&self, buffer: &mut [u8]) -> io::Result { @@ -51,7 +70,27 @@ impl FileDesc { } } -impl Drop for FileDesc { +#[cfg(not(feature = "libc"))] +impl FileDesc<'_> { + pub fn read(&self, buffer: &mut [u8]) -> io::Result { + let fd = match self { + FileDesc::Owned(fd) => fd.as_fd(), + FileDesc::Borrowed(fd) => fd.as_fd(), + }; + let result = rustix::io::read(fd, buffer)?; + Ok(result) + } + + pub fn raw_fd(&self) -> RawFd { + match self { + FileDesc::Owned(fd) => fd.as_raw_fd(), + FileDesc::Borrowed(fd) => fd.as_raw_fd(), + } + } +} + +#[cfg(feature = "libc")] +impl Drop for FileDesc<'_> { fn drop(&mut self) { if self.close_on_drop { // Note that errors are ignored when closing a file descriptor. The @@ -64,14 +103,25 @@ impl Drop for FileDesc { } } -impl AsRawFd for FileDesc { +impl AsRawFd for FileDesc<'_> { fn as_raw_fd(&self) -> RawFd { self.raw_fd() } } +#[cfg(not(feature = "libc"))] +impl AsFd for FileDesc<'_> { + fn as_fd(&self) -> BorrowedFd<'_> { + match self { + FileDesc::Owned(fd) => fd.as_fd(), + FileDesc::Borrowed(fd) => fd.as_fd(), + } + } +} + +#[cfg(feature = "libc")] /// Creates a file descriptor pointing to the standard input or `/dev/tty`. -pub fn tty_fd() -> io::Result { +pub fn tty_fd() -> io::Result> { let (fd, close_on_drop) = if unsafe { libc::isatty(libc::STDIN_FILENO) == 1 } { (libc::STDIN_FILENO, false) } else { @@ -87,3 +137,18 @@ pub fn tty_fd() -> io::Result { Ok(FileDesc::new(fd, close_on_drop)) } + +#[cfg(not(feature = "libc"))] +/// Creates a file descriptor pointing to the standard input or `/dev/tty`. +pub fn tty_fd() -> io::Result> { + use std::fs::File; + + let stdin = rustix::stdio::stdin(); + let fd = if rustix::termios::isatty(stdin) { + FileDesc::Borrowed(stdin) + } else { + let dev_tty = File::options().read(true).write(true).open("/dev/tty")?; + FileDesc::Owned(dev_tty.into()) + }; + Ok(fd) +} diff --git a/src/terminal/sys/unix.rs b/src/terminal/sys/unix.rs index ed545c5b..7129730a 100644 --- a/src/terminal/sys/unix.rs +++ b/src/terminal/sys/unix.rs @@ -4,16 +4,24 @@ use crate::terminal::{ sys::file_descriptor::{tty_fd, FileDesc}, WindowSize, }; +#[cfg(feature = "libc")] 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::{IntoRawFd, RawFd}; +#[cfg(not(feature = "libc"))] +use rustix::{ + fd::AsFd, + termios::{Termios, Winsize}, +}; -use std::{io, mem, process}; +use std::{fs::File, io, process}; +#[cfg(feature = "libc")] +use std::{ + mem, + os::unix::io::{IntoRawFd, RawFd}, +}; // Some(Termios) -> we're in the raw mode and this is the previous mode // None -> we're not in the raw mode @@ -23,6 +31,7 @@ pub(crate) fn is_raw_mode_enabled() -> bool { TERMINAL_MODE_PRIOR_RAW_MODE.lock().is_some() } +#[cfg(feature = "libc")] impl From for WindowSize { fn from(size: winsize) -> WindowSize { WindowSize { @@ -33,8 +42,20 @@ impl From for WindowSize { } } } +#[cfg(not(feature = "libc"))] +impl From for WindowSize { + fn from(size: Winsize) -> WindowSize { + WindowSize { + columns: size.ws_col, + rows: size.ws_row, + width: size.ws_xpixel, + height: size.ws_ypixel, + } + } +} #[allow(clippy::useless_conversion)] +#[cfg(feature = "libc")] pub(crate) fn window_size() -> io::Result { // http://rosettacode.org/wiki/Terminal_control/Dimensions#Library:_BSD_libc let mut size = winsize { @@ -59,6 +80,19 @@ pub(crate) fn window_size() -> io::Result { Err(std::io::Error::last_os_error().into()) } +#[cfg(not(feature = "libc"))] +pub(crate) fn window_size() -> io::Result { + let file = File::open("/dev/tty").map(|file| (FileDesc::Owned(file.into()))); + let fd = if let Ok(file) = &file { + file.as_fd() + } else { + // Fallback to libc::STDOUT_FILENO if /dev/tty is missing + rustix::stdio::stdout() + }; + let size = rustix::termios::tcgetwinsize(fd)?; + Ok(size.into()) +} + #[allow(clippy::useless_conversion)] pub(crate) fn size() -> io::Result<(u16, u16)> { if let Ok(window_size) = window_size() { @@ -68,9 +102,9 @@ pub(crate) fn size() -> io::Result<(u16, u16)> { tput_size().ok_or_else(|| std::io::Error::last_os_error().into()) } +#[cfg(feature = "libc")] pub(crate) fn enable_raw_mode() -> io::Result<()> { let mut original_mode = TERMINAL_MODE_PRIOR_RAW_MODE.lock(); - if original_mode.is_some() { return Ok(()); } @@ -79,13 +113,27 @@ pub(crate) fn enable_raw_mode() -> io::Result<()> { let fd = tty.raw_fd(); let mut ios = get_terminal_attr(fd)?; let original_mode_ios = ios; - raw_terminal_attr(&mut ios); set_terminal_attr(fd, &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); + Ok(()) +} +#[cfg(not(feature = "libc"))] +pub(crate) fn enable_raw_mode() -> io::Result<()> { + let mut original_mode = TERMINAL_MODE_PRIOR_RAW_MODE.lock(); + if original_mode.is_some() { + return Ok(()); + } + + let tty = tty_fd()?; + let mut ios = get_terminal_attr(&tty)?; + let original_mode_ios = ios.clone(); + ios.make_raw(); + 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); Ok(()) } @@ -94,16 +142,39 @@ pub(crate) fn enable_raw_mode() -> io::Result<()> { /// More precisely, reset the whole termios mode to what it was before the first call /// to [enable_raw_mode]. If you don't mess with termios outside of crossterm, it's /// effectively disabling the raw mode and doing nothing else. +#[cfg(feature = "libc")] pub(crate) fn disable_raw_mode() -> io::Result<()> { let mut original_mode = TERMINAL_MODE_PRIOR_RAW_MODE.lock(); - if let Some(original_mode_ios) = original_mode.as_ref() { let tty = tty_fd()?; set_terminal_attr(tty.raw_fd(), original_mode_ios)?; // Keep it last - remove the original mode only if we were able to switch back *original_mode = None; } + Ok(()) +} + +#[cfg(not(feature = "libc"))] +pub(crate) fn disable_raw_mode() -> io::Result<()> { + let mut original_mode = TERMINAL_MODE_PRIOR_RAW_MODE.lock(); + if let Some(original_mode_ios) = original_mode.as_ref() { + let tty = tty_fd()?; + 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; + } + Ok(()) +} + +#[cfg(not(feature = "libc"))] +fn get_terminal_attr(fd: impl AsFd) -> io::Result { + let result = rustix::termios::tcgetattr(fd)?; + Ok(result) +} +#[cfg(not(feature = "libc"))] +fn set_terminal_attr(fd: impl AsFd, termios: &Termios) -> io::Result<()> { + rustix::termios::tcsetattr(fd, rustix::termios::OptionalActions::Now, termios)?; Ok(()) } @@ -214,11 +285,13 @@ fn tput_size() -> Option<(u16, u16)> { } } +#[cfg(feature = "libc")] // Transform the given mode into an raw mode (non-canonical) mode. fn raw_terminal_attr(termios: &mut Termios) { unsafe { cfmakeraw(termios) } } +#[cfg(feature = "libc")] fn get_terminal_attr(fd: RawFd) -> io::Result { unsafe { let mut termios = mem::zeroed(); @@ -227,10 +300,12 @@ fn get_terminal_attr(fd: RawFd) -> io::Result { } } +#[cfg(feature = "libc")] fn set_terminal_attr(fd: RawFd, termios: &Termios) -> io::Result<()> { wrap_with_result(unsafe { tcsetattr(fd, TCSANOW, termios) }) } +#[cfg(feature = "libc")] fn wrap_with_result(result: i32) -> io::Result<()> { if result == -1 { Err(io::Error::last_os_error()) diff --git a/src/tty.rs b/src/tty.rs index 78e32aae..5a710b4a 100644 --- a/src/tty.rs +++ b/src/tty.rs @@ -26,7 +26,7 @@ pub trait IsTty { /// On UNIX, the `isatty()` function returns true if a file /// descriptor is a terminal. -#[cfg(unix)] +#[cfg(all(unix, feature = "libc"))] impl IsTty for S { fn is_tty(&self) -> bool { let fd = self.as_raw_fd(); @@ -34,6 +34,14 @@ impl IsTty for S { } } +#[cfg(all(unix, not(feature = "libc")))] +impl IsTty for S { + fn is_tty(&self) -> bool { + let fd = self.as_raw_fd(); + rustix::termios::isatty(unsafe { std::os::unix::io::BorrowedFd::borrow_raw(fd) }) + } +} + /// On windows, `GetConsoleMode` will return true if we are in a terminal. /// Otherwise false. #[cfg(windows)]