blob: 375706ef98e00ed4efcb0ba74a0cbd234350ce5a [file]
//! Futures [`Stream`] adapter for terminal events.
//!
//! This module is available with the `event-stream` feature. It adapts the blocking
//! [`EventReader`] API into a stream by parking a helper thread on the platform event source and
//! waking the async task when matching input arrives.
//!
//! # Implementation Notes
//!
//! This is intentionally close to [crossterm's event stream]. The Termina-specific part is the
//! shared [`EventReader`], which was added so the same reader model could support downstream
//! integration tests as well as normal terminal event streams.
//!
//! [crossterm's event stream]: https://docs.rs/crossterm/latest/crossterm/event/
use std::{
io,
pin::Pin,
sync::{
atomic::{AtomicBool, Ordering},
mpsc::{self, SyncSender},
Arc,
},
task::{Context, Poll},
thread,
time::Duration,
};
use futures_core::Stream;
use super::{reader::EventReader, source::PlatformWaker, Event};
/// A stream of [`Event`] values received from the terminal.
///
/// This type is only available if the `event-stream` feature is enabled.
///
/// Create an event stream for a terminal by passing the reader from
/// [`crate::Terminal::event_reader`] into [`EventStream::new`] with a filter.
///
/// # Examples
///
/// Requires the `event-stream` feature and an async runtime.
///
/// ```ignore
/// use futures_lite::StreamExt as _;
/// use termina::{Event, EventStream, PlatformTerminal, Terminal};
///
/// # async fn demo() -> std::io::Result<()> {
/// let reader = PlatformTerminal::new()?.event_reader();
/// let mut stream = EventStream::new(reader, |_| true);
/// while let Some(Ok(event)) = stream.next().await {
/// if matches!(event, Event::FocusOut) {
/// break;
/// }
/// }
/// # Ok(())
/// # }
/// ```
pub struct EventStream {
waker: PlatformWaker,
filter: Arc<dyn Fn(&Event) -> bool>,
reader: EventReader,
stream_wake_task_executed: Arc<AtomicBool>,
stream_wake_task_should_shutdown: Arc<AtomicBool>,
task_sender: SyncSender<Task>,
}
/// Internal task handed to the helper thread managing the blocking poll.
#[derive(Debug)]
struct Task {
stream_waker: std::task::Waker,
stream_wake_task_executed: Arc<AtomicBool>,
stream_wake_task_should_shutdown: Arc<AtomicBool>,
}
impl EventStream {
/// Creates a stream backed by `reader` that only yields events accepted by `filter`.
pub fn new<F>(reader: EventReader, filter: F) -> Self
where
F: Fn(&Event) -> bool + Send + Sync + 'static,
{
let filter = Arc::new(filter);
let waker = reader.waker();
let (task_sender, receiver) = mpsc::sync_channel::<Task>(1);
let task_reader = reader.clone();
let task_filter = filter.clone();
thread::spawn(move || {
while let Ok(task) = receiver.recv() {
loop {
if let Ok(true) = task_reader.poll(None, &*task_filter) {
break;
}
if task.stream_wake_task_should_shutdown.load(Ordering::SeqCst) {
break;
}
}
task.stream_wake_task_executed
.store(false, Ordering::SeqCst);
task.stream_waker.wake();
}
});
Self {
waker,
filter,
reader,
stream_wake_task_executed: Default::default(),
stream_wake_task_should_shutdown: Default::default(),
task_sender,
}
}
}
impl Drop for EventStream {
fn drop(&mut self) {
self.stream_wake_task_should_shutdown
.store(true, Ordering::SeqCst);
let _ = self.waker.wake();
}
}
impl Stream for EventStream {
type Item = io::Result<Event>;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
match self
.reader
.poll(Some(Duration::from_secs(0)), &*self.filter)
{
Ok(true) => match self.reader.read(&*self.filter) {
Ok(event) => Poll::Ready(Some(Ok(event))),
Err(err) => Poll::Ready(Some(Err(err))),
},
Ok(false) => {
if !self
.stream_wake_task_executed
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
.unwrap_or_else(|x| x)
{
self.stream_wake_task_should_shutdown
.store(false, Ordering::SeqCst);
let _ = self.task_sender.send(Task {
stream_waker: cx.waker().clone(),
stream_wake_task_executed: self.stream_wake_task_executed.clone(),
stream_wake_task_should_shutdown: self
.stream_wake_task_should_shutdown
.clone(),
});
}
Poll::Pending
}
Err(err) => Poll::Ready(Some(Err(err))),
}
}
}