feat: add try_read function (#1003)

This adds a new function, `try_read`, that returns `Some(Event)` from the event queue if events are present, and `None` otherwise. This can be used to process multiple events after polling without using a blocking read.

```rust
if poll(Duration::from_millis(100))? {
    // Fetch *all* available events, stopping if this would block
    while let Some(event) = try_read() {
        // ... process the event ...
    }
}
```

Closes #972
This commit is contained in:
gavincrawford 2025-07-24 22:22:59 -06:00 committed by GitHub
parent 2ba0160831
commit 6af9116b6a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 104 additions and 21 deletions

View File

@ -132,7 +132,11 @@ use derive_more::derive::IsVariant;
#[cfg(feature = "event-stream")]
pub use stream::EventStream;
use crate::{csi, event::filter::EventFilter, Command};
use crate::{
csi,
event::{filter::EventFilter, internal::InternalEvent},
Command,
};
use std::fmt::{self, Display};
use std::time::Duration;
@ -225,7 +229,37 @@ pub fn poll(timeout: Duration) -> std::io::Result<bool> {
/// ```
pub fn read() -> std::io::Result<Event> {
match internal::read(&EventFilter)? {
internal::InternalEvent::Event(event) => Ok(event),
InternalEvent::Event(event) => Ok(event),
#[cfg(unix)]
_ => unreachable!(),
}
}
/// Attempts to read a single [`Event`](enum.Event.html) without blocking the thread.
///
/// If no event is found, `None` is returned.
///
/// # Examples
///
/// ```no_run
/// use crossterm::event::{try_read, poll};
/// use std::{io, time::Duration};
///
/// fn print_all_events() -> io::Result<bool> {
/// loop {
/// if poll(Duration::from_millis(100))? {
/// // Fetch *all* available events at once
/// while let Some(event) = try_read() {
/// // ...
/// }
/// }
/// }
/// }
/// ```
pub fn try_read() -> Option<Event> {
match internal::try_read(&EventFilter) {
Some(InternalEvent::Event(event)) => Some(event),
None => None,
#[cfg(unix)]
_ => unreachable!(),
}

View File

@ -52,6 +52,15 @@ where
reader.read(filter)
}
/// Reads a single `InternalEvent`. Non-blocking.
pub(crate) fn try_read<F>(filter: &F) -> Option<InternalEvent>
where
F: Filter,
{
let mut reader = lock_event_reader();
reader.try_read(filter)
}
/// An internal event.
///
/// Encapsulates publicly available `Event` with additional internal

View File

@ -96,35 +96,53 @@ impl InternalEventReader {
}
}
/// Blocks the thread until a valid `InternalEvent` can be read.
///
/// Internally, we use `try_read`, which buffers the events that do not fulfill the filter
/// conditions to prevent stalling the thread in an infinite loop.
pub(crate) fn read<F>(&mut self, filter: &F) -> io::Result<InternalEvent>
where
F: Filter,
{
let mut skipped_events = VecDeque::new();
// blocks the thread until a valid event is found
loop {
while let Some(event) = self.events.pop_front() {
if filter.eval(&event) {
while let Some(event) = skipped_events.pop_front() {
self.events.push_back(event);
}
return Ok(event);
} else {
// We can not directly write events back to `self.events`.
// If we did, we would put our self's into an endless loop
// that would enqueue -> dequeue -> enqueue etc.
// This happens because `poll` in this function will always return true if there are events in it's.
// And because we just put the non-fulfilling event there this is going to be the case.
// Instead we can store them into the temporary buffer,
// and then when the filter is fulfilled write all events back in order.
skipped_events.push_back(event);
}
if let Some(event) = self.try_read(filter) {
return Ok(event);
}
let _ = self.poll(None, filter)?;
}
}
/// Attempts to read the first valid `InternalEvent`.
///
/// This function checks all events in the queue, and stores events that do not match the
/// filter in a buffer to be added back to the queue after all items have been evaluated. We
/// must buffer non-fulfilling events because, if added directly back to the queue, they would
/// result in an infinite loop, rechecking events that have already been evaluated against the
/// filter.
pub(crate) fn try_read<F>(&mut self, filter: &F) -> Option<InternalEvent>
where
F: Filter,
{
// check all events, storing events that do not match the filter in the `skipped_events`
// buffer to be added back later
let mut skipped_events = Vec::new();
let mut result = None;
while let Some(event) = self.events.pop_front() {
if filter.eval(&event) {
result = Some(event);
break;
}
skipped_events.push(event);
}
// push all skipped events back to the event queue
self.events.extend(skipped_events);
result
}
}
#[cfg(test)]
@ -232,6 +250,28 @@ mod tests {
assert_eq!(reader.read(&InternalEventFilter).unwrap(), SKIPPED_EVENT);
}
#[test]
#[cfg(unix)]
fn test_try_read_does_not_consume_skipped_event() {
const SKIPPED_EVENT: InternalEvent = InternalEvent::Event(Event::Resize(10, 10));
const CURSOR_EVENT: InternalEvent = InternalEvent::CursorPosition(10, 20);
let mut reader = InternalEventReader {
events: vec![SKIPPED_EVENT, CURSOR_EVENT].into(),
source: None,
skipped_events: Vec::with_capacity(32),
};
assert_eq!(
reader.try_read(&CursorPositionFilter).unwrap(),
CURSOR_EVENT
);
assert_eq!(
reader.try_read(&InternalEventFilter).unwrap(),
SKIPPED_EVENT
);
}
#[test]
fn test_poll_timeouts_if_source_has_no_events() {
let source = FakeSource::default();