Skip to content

Commit

Permalink
fix: standardize usage of /dev/tty everywhere
Browse files Browse the repository at this point in the history
  • Loading branch information
aschey committed Dec 15, 2024
1 parent fc8f977 commit 35d0e4a
Show file tree
Hide file tree
Showing 6 changed files with 143 additions and 85 deletions.
13 changes: 7 additions & 6 deletions src/cursor/sys/unix.rs
Original file line number Diff line number Diff line change
@@ -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).
Expand All @@ -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) => {
Expand Down
42 changes: 21 additions & 21 deletions src/event/read.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<InternalEvent>,
source: Option<Box<dyn EventSource>>,
source: io::Result<Box<dyn EventSource>>,
skipped_events: Vec<InternalEvent>,
}

Expand All @@ -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<dyn EventSource>);
let source = source.map(|x| Box::new(x) as Box<dyn EventSource>);

InternalEventReader {
source,
Expand Down Expand Up @@ -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:?}"),
))
}
};
Expand Down Expand Up @@ -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),
};

Expand All @@ -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),
};

Expand All @@ -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),
};

Expand All @@ -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),
};

Expand All @@ -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),
};

Expand All @@ -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),
};

Expand All @@ -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),
};

Expand All @@ -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),
};

Expand All @@ -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),
};

Expand All @@ -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),
};

Expand All @@ -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),
};

Expand All @@ -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),
};

Expand All @@ -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),
};

Expand All @@ -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),
};

Expand All @@ -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),
};

Expand Down
26 changes: 24 additions & 2 deletions src/event/source/unix/mio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -33,8 +33,30 @@ pub(crate) struct UnixInternalEventSource {
}

impl UnixInternalEventSource {
#[cfg(target_os = "macos")]
pub fn new() -> io::Result<Self> {
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<Self> {
UnixInternalEventSource::from_file_descriptor(
crate::terminal::sys::file_descriptor::tty_fd_in()?,
)
}

pub(crate) fn from_file_descriptor(input_fd: FileDesc<'static>) -> io::Result<Self> {
Expand Down
4 changes: 2 additions & 2 deletions src/event/source/unix/tty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down Expand Up @@ -57,7 +57,7 @@ fn nonblocking_unix_pair() -> io::Result<(UnixStream, UnixStream)> {

impl UnixInternalEventSource {
pub fn new() -> io::Result<Self> {
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<Self> {
Expand Down
93 changes: 76 additions & 17 deletions src/terminal/sys/file_descriptor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,22 @@ impl FileDesc<'_> {
}
}

pub fn write(&self, buffer: &[u8]) -> io::Result<usize> {
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
Expand All @@ -81,6 +97,15 @@ impl FileDesc<'_> {
Ok(result)
}

pub fn write(&self, buffer: &[u8]) -> io::Result<usize> {
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(),
Expand Down Expand Up @@ -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<FileDesc<'static>> {
let (fd, close_on_drop) = if unsafe { libc::isatty(libc::STDIN_FILENO) == 1 } {
(libc::STDIN_FILENO, false)
pub fn tty_fd_in() -> io::Result<FileDesc<'static>> {
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<FileDesc<'static>> {
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<FileDesc<'static>> {
pub fn tty_fd_in() -> io::Result<FileDesc<'static>> {
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<FileDesc<'static>> {
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)
}
Loading

0 comments on commit 35d0e4a

Please sign in to comment.