From 35d0e4abd2c55eb214d414f0c2ab194020cb7136 Mon Sep 17 00:00:00 2001 From: Austin Schey Date: Sun, 15 Dec 2024 15:30:19 -0800 Subject: [PATCH] fix: standardize usage of /dev/tty everywhere --- src/cursor/sys/unix.rs | 13 ++-- src/event/read.rs | 42 ++++++------- src/event/source/unix/mio.rs | 26 +++++++- src/event/source/unix/tty.rs | 4 +- src/terminal/sys/file_descriptor.rs | 93 +++++++++++++++++++++++------ src/terminal/sys/unix.rs | 50 ++++------------ 6 files changed, 143 insertions(+), 85 deletions(-) diff --git a/src/cursor/sys/unix.rs b/src/cursor/sys/unix.rs index 473421207..8d5abecd4 100644 --- a/src/cursor/sys/unix.rs +++ b/src/cursor/sys/unix.rs @@ -1,11 +1,14 @@ use std::{ - io::{self, Error, ErrorKind, Write}, + io::{self, Error, ErrorKind}, time::Duration, }; use crate::{ event::{filter::CursorPositionFilter, poll_internal, read_internal, InternalEvent}, - terminal::{disable_raw_mode, enable_raw_mode, sys::is_raw_mode_enabled}, + terminal::{ + disable_raw_mode, enable_raw_mode, + sys::{file_descriptor::tty_fd_out, is_raw_mode_enabled}, + }, }; /// Returns the cursor position (column, row). @@ -31,10 +34,8 @@ fn read_position() -> io::Result<(u16, u16)> { fn read_position_raw() -> io::Result<(u16, u16)> { // Use `ESC [ 6 n` to and retrieve the cursor position. - let mut stdout = io::stdout(); - stdout.write_all(b"\x1B[6n")?; - stdout.flush()?; - + let stdout = tty_fd_out()?; + stdout.write(b"\x1B[6n")?; loop { match poll_internal(Some(Duration::from_millis(2000)), &CursorPositionFilter) { Ok(true) => { diff --git a/src/event/read.rs b/src/event/read.rs index 22f989b4d..10be8b1ff 100644 --- a/src/event/read.rs +++ b/src/event/read.rs @@ -11,7 +11,7 @@ use crate::event::{filter::Filter, source::EventSource, timeout::PollTimeout, In /// Can be used to read `InternalEvent`s. pub(crate) struct InternalEventReader { events: VecDeque, - source: Option>, + source: io::Result>, skipped_events: Vec, } @@ -22,7 +22,7 @@ impl Default for InternalEventReader { #[cfg(unix)] let source = UnixInternalEventSource::new(); - let source = source.ok().map(|x| Box::new(x) as Box); + let source = source.map(|x| Box::new(x) as Box); InternalEventReader { source, @@ -50,11 +50,11 @@ impl InternalEventReader { } let event_source = match self.source.as_mut() { - Some(source) => source, - None => { + Ok(source) => source, + Err(e) => { return Err(std::io::Error::new( - std::io::ErrorKind::Other, - "Failed to initialize input reader", + e.kind(), + format!("Failed to initialize input reader: {e:?}"), )) } }; @@ -147,7 +147,7 @@ mod tests { fn test_poll_fails_without_event_source() { let mut reader = InternalEventReader { events: VecDeque::new(), - source: None, + source: Err(io::Error::new(io::ErrorKind::Other, "not initialized")), skipped_events: Vec::with_capacity(32), }; @@ -164,7 +164,7 @@ mod tests { fn test_poll_returns_true_for_matching_event_in_queue_at_front() { let mut reader = InternalEventReader { events: vec![InternalEvent::Event(Event::Resize(10, 10))].into(), - source: None, + source: Err(io::Error::new(io::ErrorKind::Other, "not initialized")), skipped_events: Vec::with_capacity(32), }; @@ -180,7 +180,7 @@ mod tests { InternalEvent::CursorPosition(10, 20), ] .into(), - source: None, + source: Err(io::Error::new(io::ErrorKind::Other, "not initialized")), skipped_events: Vec::with_capacity(32), }; @@ -193,7 +193,7 @@ mod tests { let mut reader = InternalEventReader { events: vec![EVENT].into(), - source: None, + source: Err(io::Error::new(io::ErrorKind::Other, "not initialized")), skipped_events: Vec::with_capacity(32), }; @@ -207,7 +207,7 @@ mod tests { let mut reader = InternalEventReader { events: vec![InternalEvent::Event(Event::Resize(10, 10)), CURSOR_EVENT].into(), - source: None, + source: Err(io::Error::new(io::ErrorKind::Other, "not initialized")), skipped_events: Vec::with_capacity(32), }; @@ -222,7 +222,7 @@ mod tests { let mut reader = InternalEventReader { events: vec![SKIPPED_EVENT, CURSOR_EVENT].into(), - source: None, + source: Err(io::Error::new(io::ErrorKind::Other, "not initialized")), skipped_events: Vec::with_capacity(32), }; @@ -236,7 +236,7 @@ mod tests { let mut reader = InternalEventReader { events: VecDeque::new(), - source: Some(Box::new(source)), + source: Ok(Box::new(source)), skipped_events: Vec::with_capacity(32), }; @@ -251,7 +251,7 @@ mod tests { let mut reader = InternalEventReader { events: VecDeque::new(), - source: Some(Box::new(source)), + source: Ok(Box::new(source)), skipped_events: Vec::with_capacity(32), }; @@ -269,7 +269,7 @@ mod tests { let mut reader = InternalEventReader { events: VecDeque::new(), - source: Some(Box::new(source)), + source: Ok(Box::new(source)), skipped_events: Vec::with_capacity(32), }; @@ -284,7 +284,7 @@ mod tests { let mut reader = InternalEventReader { events: VecDeque::new(), - source: Some(Box::new(source)), + source: Ok(Box::new(source)), skipped_events: Vec::with_capacity(32), }; @@ -301,7 +301,7 @@ mod tests { let mut reader = InternalEventReader { events: VecDeque::new(), - source: Some(Box::new(source)), + source: Ok(Box::new(source)), skipped_events: Vec::with_capacity(32), }; @@ -317,7 +317,7 @@ mod tests { fn test_poll_propagates_error() { let mut reader = InternalEventReader { events: VecDeque::new(), - source: Some(Box::new(FakeSource::new(&[]))), + source: Ok(Box::new(FakeSource::new(&[]))), skipped_events: Vec::with_capacity(32), }; @@ -334,7 +334,7 @@ mod tests { fn test_read_propagates_error() { let mut reader = InternalEventReader { events: VecDeque::new(), - source: Some(Box::new(FakeSource::new(&[]))), + source: Ok(Box::new(FakeSource::new(&[]))), skipped_events: Vec::with_capacity(32), }; @@ -355,7 +355,7 @@ mod tests { let mut reader = InternalEventReader { events: VecDeque::new(), - source: Some(Box::new(source)), + source: Ok(Box::new(source)), skipped_events: Vec::with_capacity(32), }; @@ -374,7 +374,7 @@ mod tests { let mut reader = InternalEventReader { events: VecDeque::new(), - source: Some(Box::new(source)), + source: Ok(Box::new(source)), skipped_events: Vec::with_capacity(32), }; diff --git a/src/event/source/unix/mio.rs b/src/event/source/unix/mio.rs index 6d19e0c5c..0293bbdac 100644 --- a/src/event/source/unix/mio.rs +++ b/src/event/source/unix/mio.rs @@ -8,7 +8,7 @@ use crate::event::sys::Waker; use crate::event::{ source::EventSource, sys::unix::parse::parse_event, timeout::PollTimeout, Event, InternalEvent, }; -use crate::terminal::sys::file_descriptor::{tty_fd, FileDesc}; +use crate::terminal::sys::file_descriptor::FileDesc; // Tokens to identify file descriptor const TTY_TOKEN: Token = Token(0); @@ -33,8 +33,30 @@ pub(crate) struct UnixInternalEventSource { } impl UnixInternalEventSource { + #[cfg(target_os = "macos")] pub fn new() -> io::Result { - UnixInternalEventSource::from_file_descriptor(tty_fd()?) + // Polling /dev/tty with mio is unsupported on MacOS so we must explicitly use stdin + use crate::tty::IsTty; + #[cfg(feature = "libc")] + let fd = FileDesc::new(libc::STDIN_FILENO, false); + #[cfg(not(feature = "libc"))] + let fd = FileDesc::Borrowed(rustix::stdio::stdin()); + + if !fd.is_tty() { + return Err(io::Error::new( + io::ErrorKind::Unsupported, + "The 'use-dev-tty' feature must be enabled to read terminal events on MacOS when stdin is not a tty.", + )); + } + + UnixInternalEventSource::from_file_descriptor(fd) + } + + #[cfg(not(target_os = "macos"))] + pub fn new() -> io::Result { + UnixInternalEventSource::from_file_descriptor( + crate::terminal::sys::file_descriptor::tty_fd_in()?, + ) } pub(crate) fn from_file_descriptor(input_fd: FileDesc<'static>) -> io::Result { diff --git a/src/event/source/unix/tty.rs b/src/event/source/unix/tty.rs index 03d76b401..29503e75f 100644 --- a/src/event/source/unix/tty.rs +++ b/src/event/source/unix/tty.rs @@ -14,7 +14,7 @@ use filedescriptor::{poll, pollfd, POLLIN}; #[cfg(feature = "event-stream")] use crate::event::sys::Waker; use crate::event::{source::EventSource, sys::unix::parse::parse_event, InternalEvent}; -use crate::terminal::sys::file_descriptor::{tty_fd, FileDesc}; +use crate::terminal::sys::file_descriptor::{tty_fd_in, FileDesc}; /// Holds a prototypical Waker and a receiver we can wait on when doing select(). #[cfg(feature = "event-stream")] @@ -57,7 +57,7 @@ fn nonblocking_unix_pair() -> io::Result<(UnixStream, UnixStream)> { impl UnixInternalEventSource { pub fn new() -> io::Result { - UnixInternalEventSource::from_file_descriptor(tty_fd()?) + UnixInternalEventSource::from_file_descriptor(tty_fd_in()?) } pub(crate) fn from_file_descriptor(input_fd: FileDesc<'static>) -> io::Result { diff --git a/src/terminal/sys/file_descriptor.rs b/src/terminal/sys/file_descriptor.rs index baff266c6..04a6a801b 100644 --- a/src/terminal/sys/file_descriptor.rs +++ b/src/terminal/sys/file_descriptor.rs @@ -64,6 +64,22 @@ impl FileDesc<'_> { } } + pub fn write(&self, buffer: &[u8]) -> io::Result { + let result = unsafe { + libc::write( + self.fd, + buffer.as_ptr() as *const libc::c_void, + buffer.len() as size_t, + ) + }; + + if result < 0 { + Err(io::Error::last_os_error()) + } else { + Ok(result as usize) + } + } + /// Returns the underlying file descriptor. pub fn raw_fd(&self) -> RawFd { self.fd @@ -81,6 +97,15 @@ impl FileDesc<'_> { Ok(result) } + pub fn write(&self, buffer: &[u8]) -> io::Result { + let fd = match self { + FileDesc::Owned(fd) => fd.as_fd(), + FileDesc::Borrowed(fd) => fd.as_fd(), + }; + let result = rustix::io::write(fd, buffer)?; + Ok(result) + } + pub fn raw_fd(&self) -> RawFd { match self { FileDesc::Owned(fd) => fd.as_raw_fd(), @@ -121,34 +146,68 @@ impl AsFd for FileDesc<'_> { #[cfg(feature = "libc")] /// 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) +pub fn tty_fd_in() -> io::Result> { + let (fd, close_on_drop) = if let Ok(file) = fs::OpenOptions::new() + .read(true) + .write(true) + .open("/dev/tty") + { + (file.into_raw_fd(), true) } else { - ( - fs::OpenOptions::new() - .read(true) - .write(true) - .open("/dev/tty")? - .into_raw_fd(), - true, - ) + (libc::STDIN_FILENO, false) }; + Ok(FileDesc::new(fd, close_on_drop)) +} +#[cfg(feature = "libc")] +/// Creates a file descriptor pointing to the standard output or `/dev/tty`. +pub fn tty_fd_out() -> io::Result> { + let (fd, close_on_drop) = if let Ok(file) = fs::OpenOptions::new() + .read(true) + .write(true) + .open("/dev/tty") + { + (file.into_raw_fd(), true) + } else { + (libc::STDOUT_FILENO, false) + }; 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> { +pub fn tty_fd_in() -> io::Result> { + use std::fs::File; + + let file = File::options() + .read(true) + .write(true) + .open("/dev/tty") + .map(|file| (FileDesc::Owned(file.into()))); + let fd = if let Ok(file) = file { + file + } else { + // Fallback to stdin if /dev/tty is missing + FileDesc::Borrowed(rustix::stdio::stdin()) + }; + Ok(fd) +} + +#[cfg(not(feature = "libc"))] +/// Creates a file descriptor pointing to the standard output or `/dev/tty`. +pub fn tty_fd_out() -> io::Result> { use std::fs::File; - let stdin = rustix::stdio::stdin(); - let fd = if rustix::termios::isatty(stdin) { - FileDesc::Borrowed(stdin) + let file = File::options() + .read(true) + .write(true) + .open("/dev/tty") + .map(|file| (FileDesc::Owned(file.into()))); + let fd = if let Ok(file) = file { + file } else { - let dev_tty = File::options().read(true).write(true).open("/dev/tty")?; - FileDesc::Owned(dev_tty.into()) + // Fallback to stdout if /dev/tty is missing + FileDesc::Borrowed(rustix::stdio::stdout()) }; Ok(fd) } diff --git a/src/terminal/sys/unix.rs b/src/terminal/sys/unix.rs index 7129730a6..a0d0c6c34 100644 --- a/src/terminal/sys/unix.rs +++ b/src/terminal/sys/unix.rs @@ -1,13 +1,12 @@ //! UNIX related logic for terminal manipulation. use crate::terminal::{ - sys::file_descriptor::{tty_fd, FileDesc}, + sys::file_descriptor::{tty_fd_in, tty_fd_out}, WindowSize, }; #[cfg(feature = "libc")] use libc::{ - cfmakeraw, ioctl, tcgetattr, tcsetattr, termios as Termios, winsize, STDOUT_FILENO, TCSANOW, - TIOCGWINSZ, + cfmakeraw, ioctl, tcgetattr, tcsetattr, termios as Termios, winsize, TCSANOW, TIOCGWINSZ, }; use parking_lot::Mutex; #[cfg(not(feature = "libc"))] @@ -16,12 +15,9 @@ use rustix::{ termios::{Termios, Winsize}, }; -use std::{fs::File, io, process}; +use std::{io, process}; #[cfg(feature = "libc")] -use std::{ - mem, - os::unix::io::{IntoRawFd, RawFd}, -}; +use std::{mem, os::unix::io::RawFd}; // Some(Termios) -> we're in the raw mode and this is the previous mode // None -> we're not in the raw mode @@ -65,15 +61,9 @@ pub(crate) fn window_size() -> io::Result { ws_ypixel: 0, }; - let file = File::open("/dev/tty").map(|file| (FileDesc::new(file.into_raw_fd(), true))); - let fd = if let Ok(file) = &file { - file.raw_fd() - } else { - // Fallback to libc::STDOUT_FILENO if /dev/tty is missing - STDOUT_FILENO - }; + let fd = tty_fd_out()?; - if wrap_with_result(unsafe { ioctl(fd, TIOCGWINSZ.into(), &mut size) }).is_ok() { + if wrap_with_result(unsafe { ioctl(fd.raw_fd(), TIOCGWINSZ.into(), &mut size) }).is_ok() { return Ok(size.into()); } @@ -82,13 +72,7 @@ pub(crate) fn window_size() -> io::Result { #[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 fd = tty_fd_out()?; let size = rustix::termios::tcgetwinsize(fd)?; Ok(size.into()) } @@ -109,7 +93,7 @@ pub(crate) fn enable_raw_mode() -> io::Result<()> { return Ok(()); } - let tty = tty_fd()?; + let tty = tty_fd_in()?; let fd = tty.raw_fd(); let mut ios = get_terminal_attr(fd)?; let original_mode_ios = ios; @@ -127,7 +111,7 @@ pub(crate) fn enable_raw_mode() -> io::Result<()> { return Ok(()); } - let tty = tty_fd()?; + let tty = tty_fd_in()?; let mut ios = get_terminal_attr(&tty)?; let original_mode_ios = ios.clone(); ios.make_raw(); @@ -146,7 +130,7 @@ pub(crate) fn enable_raw_mode() -> io::Result<()> { 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()?; + let tty = tty_fd_in()?; 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; @@ -158,7 +142,7 @@ pub(crate) fn disable_raw_mode() -> io::Result<()> { 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()?; + let tty = tty_fd_in()?; 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; @@ -205,7 +189,6 @@ fn read_supports_keyboard_enhancement_raw() -> io::Result { filter::{KeyboardEnhancementFlagsFilter, PrimaryDeviceAttributesFilter}, poll_internal, read_internal, InternalEvent, }; - use std::io::Write; use std::time::Duration; // This is the recommended method for testing support for the keyboard enhancement protocol. @@ -219,15 +202,8 @@ fn read_supports_keyboard_enhancement_raw() -> io::Result { // ESC [ c Query primary device attributes. const QUERY: &[u8] = b"\x1B[?u\x1B[c"; - let result = File::open("/dev/tty").and_then(|mut file| { - file.write_all(QUERY)?; - file.flush() - }); - if result.is_err() { - let mut stdout = io::stdout(); - stdout.write_all(QUERY)?; - stdout.flush()?; - } + let tty = tty_fd_out()?; + tty.write(QUERY)?; loop { match poll_internal(