Skip to main content

uncurses/event/
source.rs

1//! Wakeable event source shared by synchronous and asynchronous readers.
2//!
3//! ## Purpose
4//!
5//! [`EventSource`] owns the input handle, waits for platform readiness, feeds a
6//! [`Decoder`], and queues typed [`Event`] values. It is the entry
7//! point for applications that want blocking, timeout-based, or wakeable event
8//! reads.
9//!
10//! ```text
11//! input fd / HANDLE ─┬─▶ Poller ── ready ──▶ read bytes ──▶ Decoder
12//! wake handle ───────┤                         │              │
13//! SIGWINCH pipe ─────┘                         └──────────────┴─▶ queue
14//!      (Unix only)        deadlines: ESC timeout and paste idle timeout
15//! ```
16//!
17//! ## Key types
18//!
19//! * [`Input`] describes the platform capabilities required from an input
20//!   handle.
21//! * [`EventSource`] stores the decoder, bounded pending-byte buffer, readiness
22//!   poller, event queue, timeout deadlines, and resize state.
23//! * [`Waker`] is a cloneable handle that interrupts an in-progress wait from
24//!   another thread.
25//!
26//! ## Lifecycle
27//!
28//! Construct with [`EventSource::new`] on the platform-specific impl. Call
29//! [`EventSource::poll`] to wait up to a timeout, drain queued events with
30//! [`EventSource::try_read`], or call [`EventSource::read`] to block until one
31//! event arrives. [`EventSource::unread`] can put an unrelated event back while
32//! code waits for a specific terminal reply.
33//!
34//! ## Gotchas
35//!
36//! [`EventSource::try_read`] is purely non-blocking: it only pops the queue.
37//! [`EventSource::poll`] performs I/O and timeout handling. The effective poll
38//! wait is shortened when a partial `ESC` sequence or open paste has an internal
39//! deadline, so a long caller timeout does not delay disambiguation.
40use std::collections::VecDeque;
41use std::io;
42use std::io::Read;
43use std::sync::Arc;
44use std::time::{Duration, Instant};
45
46#[cfg(unix)]
47use std::os::fd::AsFd;
48#[cfg(unix)]
49use std::os::fd::OwnedFd;
50#[cfg(windows)]
51use std::os::windows::io::AsHandle;
52
53use super::decode::{Decoder, is_c1_introducer};
54use super::pending::Pending;
55#[cfg(any(unix, windows))]
56use super::poll::Poller;
57#[cfg(unix)]
58use super::sigwinch as winch;
59#[cfg(unix)]
60use super::source_unix::UnixWakerInner;
61#[cfg(windows)]
62use super::source_windows::WindowsWakerInner;
63use crate::event::Event;
64#[cfg(unix)]
65use crate::terminal::Winsize;
66
67/// Platform-specific capabilities required from a Unix event input handle.
68///
69/// The handle must implement [`Read`], expose an fd through [`AsFd`], and be
70/// safe to move to the helper thread used by async streams. Any type satisfying
71/// those bounds implements this trait automatically.
72///
73/// Users normally do not implement this trait manually; pass the input half
74/// returned by the terminal API to [`EventSource::new`].
75#[cfg(unix)]
76pub trait Input: Read + AsFd + Send {}
77#[cfg(unix)]
78impl<T: Read + AsFd + Send> Input for T {}
79
80/// Platform-specific capabilities required from a Windows event input handle.
81///
82/// The handle must implement [`Read`], expose a console `HANDLE` through
83/// [`AsHandle`], and be safe to move to the helper thread used by async streams.
84/// Any type satisfying those bounds implements this trait automatically.
85///
86/// Users normally do not implement this trait manually; pass the input half
87/// returned by the terminal API to [`EventSource::new`].
88#[cfg(windows)]
89pub trait Input: Read + AsHandle + Send {}
90#[cfg(windows)]
91impl<T: Read + AsHandle + Send> Input for T {}
92
93/// Default read buffer capacity. This is the hard cap on any single
94/// sequence; the backing buffer is allocated once at construction.
95pub(super) const DEFAULT_BUFFER_CAPACITY: usize = 4096;
96
97/// Readiness slots the platform poller reports, in fixed index order.
98/// Unix watches `[input, wake, winch]`; Windows watches `[input, wake]`
99/// (resize arrives in-band as a console record, so there is no winch fd).
100#[cfg(unix)]
101pub(super) const READY_SLOTS: usize = 3;
102#[cfg(windows)]
103pub(super) const READY_SLOTS: usize = 2;
104/// Index of the input handle in the readiness slice.
105#[cfg(any(unix, windows))]
106pub(super) const READY_INPUT: usize = 0;
107/// Index of the wake handle in the readiness slice.
108#[cfg(any(unix, windows))]
109pub(super) const READY_WAKE: usize = 1;
110/// Index of the SIGWINCH pipe in the readiness slice (Unix only).
111#[cfg(unix)]
112pub(super) const READY_WINCH: usize = 2;
113
114/// Default escape-sequence timeout.
115///
116/// When the pending buffer holds a partial `ESC`-prefixed sequence or 8-bit C1
117/// introducer and no continuation arrives within this window, the source asks
118/// the decoder to resolve the buffered bytes as best-effort events. This is how
119/// a physical Escape key is distinguished from an Alt-prefixed key or CSI-style
120/// control sequence.
121///
122/// Applications that prefer more responsive Escape handling can lower this
123/// value with [`EventSource::with_esc_timeout`]; applications that need to
124/// tolerate slow byte delivery can raise it.
125pub const DEFAULT_ESC_TIMEOUT: Duration = Duration::from_millis(50);
126
127/// Default bracketed-paste idle timeout.
128///
129/// When a paste has been opened (a `PasteStart` was emitted) and no
130/// further input arrives within this window, the source synthesises a
131/// `PasteEnd`, flushes any held-back bytes as a final `PasteChunk`,
132/// and clears the decoder's paste state. Guards against terminators
133/// that never arrive (truncated stream, malformed input).
134pub const DEFAULT_PASTE_IDLE_TIMEOUT: Duration = Duration::from_secs(2);
135
136/// Cloneable handle that interrupts an in-progress [`EventSource::poll`] or
137/// [`EventSource::read`].
138///
139/// A waker is bound to one source at construction time. Calling [`Waker::wake`]
140/// makes the source's readiness wait return `Interrupted`; multiple wake calls
141/// may coalesce into one interruption. The handle is cheap to clone and can be
142/// sent to other threads.
143#[derive(Clone)]
144pub struct Waker {
145    #[cfg(unix)]
146    inner: Arc<UnixWakerInner>,
147    #[cfg(windows)]
148    inner: Arc<WindowsWakerInner>,
149    #[cfg(not(any(unix, windows)))]
150    _phantom: std::marker::PhantomData<()>,
151}
152
153impl Waker {
154    #[cfg(unix)]
155    pub(super) fn from_unix_inner(inner: Arc<UnixWakerInner>) -> Self {
156        Self { inner }
157    }
158
159    #[cfg(windows)]
160    pub(super) fn from_windows_inner(inner: Arc<WindowsWakerInner>) -> Self {
161        Self { inner }
162    }
163
164    /// Interrupt the [`EventSource`] this waker is bound to.
165    ///
166    /// A blocked [`EventSource::poll`] returns `Ok(false)` and a blocked
167    /// [`EventSource::read`] returns an [`io::ErrorKind::Interrupted`] error.
168    /// The wake does not enqueue an [`Event`] and does not mutate decoder state.
169    ///
170    /// Returns any platform error produced while signalling the wake handle.
171    pub fn wake(&self) -> io::Result<()> {
172        #[cfg(any(unix, windows))]
173        {
174            self.inner.wake()
175        }
176        #[cfg(not(any(unix, windows)))]
177        {
178            Err(io::Error::new(
179                io::ErrorKind::Unsupported,
180                "waker not supported on this platform",
181            ))
182        }
183    }
184}
185
186/// Wakeable event source backed by a platform readiness primitive.
187///
188/// `EventSource` is the synchronous owner of terminal input. It stores pending
189/// bytes, a `Decoder`, an event queue, deadline state for ambiguous `ESC`
190/// prefixes and open bracketed pastes, and the platform handles needed for
191/// wakeups and resize notifications.
192///
193/// Construct it with [`EventSource::new`]. Use [`EventSource::poll`] to perform
194/// I/O and wait for queued events, [`EventSource::try_read`] to pop an already
195/// queued event, or [`EventSource::read`] to block until one event is available.
196/// The type is generic over the platform [`Input`] handle.
197pub struct EventSource<I>
198where
199    I: Input,
200{
201    /// Owned input handle. Used both as the byte source (`Read`) and, on
202    /// Unix, as the readiness target (its fd is registered with the
203    /// [`super::poll::Poller`]).
204    #[cfg_attr(windows, allow(dead_code))]
205    pub(super) input: I,
206
207    pub(super) parser: Decoder,
208    /// Bounded read buffer. The unread slice is `pending.slice()`; new
209    /// input is written into `pending.spare_mut()`. `pending.capacity()`
210    /// is the hard cap on any single sequence and is never resized.
211    pub(super) pending: Pending,
212    pub(super) esc_timeout: Duration,
213    /// Wall-clock instant at which a buffered partial escape sequence
214    /// should be force-resolved (typically as a bare `Esc` keypress).
215    pub(super) esc_deadline: Option<Instant>,
216    /// Idle timeout applied while the decoder is in a bracketed paste.
217    /// `None` disables the safety net.
218    pub(super) paste_idle_timeout: Option<Duration>,
219    /// Wall-clock instant at which an open paste should be
220    /// force-closed (synthesised `PasteEnd`).
221    pub(super) paste_deadline: Option<Instant>,
222    pub(super) queue: VecDeque<Event>,
223    pub(super) waker: Waker,
224    /// Whether the source delivers [`Event::Resize`] from the
225    /// out-of-band kernel resize notification (`SIGWINCH` on Unix).
226    /// Defaults to `true`. Set to `false` when in-band resize reports
227    /// (DEC mode 2048) are enabled so resizes arrive solely through the
228    /// decoder and are not duplicated. No effect on Windows, where resize
229    /// is always delivered in-band through the decoder.
230    pub(super) handle_resize: bool,
231
232    // --- Unix-only state ---
233    /// Shared readiness poller watching `[input, pipe_rx, winch_rx]` (in
234    /// that index order). Held behind `Arc` so an [`super::EventStream`]
235    /// reader thread can wait on it lock-free while the source's decode
236    /// state is mutated under a separate lock.
237    #[cfg(unix)]
238    pub(super) poller: Arc<dyn Poller>,
239    #[cfg(unix)]
240    pub(super) pipe_rx: OwnedFd,
241    #[cfg(unix)]
242    pub(super) winch_rx: OwnedFd,
243    /// Held to keep the SIGWINCH write fd alive for the handler.
244    /// Dropped after `_winch_sub` so the subscription is removed
245    /// before the handler can write into a closed fd.
246    #[cfg(unix)]
247    pub(super) _winch_tx: OwnedFd,
248    /// Active SIGWINCH subscription. Dropped before `_winch_tx` per
249    /// declaration order so the handler is unregistered first.
250    #[cfg(unix)]
251    pub(super) _winch_sub: winch::Subscription,
252    #[cfg(unix)]
253    pub(super) last_size: Option<Winsize>,
254
255    // --- Windows-only state ---
256    /// Shared readiness poller watching `[input_handle, wake_event]` (in
257    /// that index order). Held behind `Arc` so an [`super::EventStream`]
258    /// reader thread can wait on it lock-free while the source's decode
259    /// state is mutated under a separate lock.
260    #[cfg(windows)]
261    pub(super) poller: Arc<dyn Poller>,
262    #[cfg(windows)]
263    pub(super) wake_event: windows_sys::Win32::Foundation::HANDLE,
264    #[cfg(windows)]
265    pub(super) vt_input: bool,
266    /// Pending high surrogate per key direction (0 = up, 1 = down).
267    /// VT input delivers astral code points as two consecutive
268    /// `KEY_EVENT` records, one per UTF-16 unit.
269    #[cfg(windows)]
270    pub(super) pending_high_surrogate: [Option<u16>; 2],
271    #[cfg(windows)]
272    pub(super) last_size: Option<(i16, i16)>,
273    #[cfg(windows)]
274    pub(super) last_mouse_buttons: u32,
275}
276
277// Drop ordering note for Unix: Rust drops fields top-to-bottom, so
278// _winch_sub (declared before _winch_tx in the struct) is dropped
279// first, removing the SIGWINCH handler before the write end of the
280// pipe is closed.
281
282// ---------------------------------------------------------------------------
283// Shared methods (platform-agnostic)
284// ---------------------------------------------------------------------------
285
286impl<I> EventSource<I>
287where
288    I: Input,
289{
290    /// Return the configured escape-sequence timeout.
291    ///
292    /// This is the duration used to disambiguate a physical Escape key from an
293    /// Alt-prefixed key or a partial control sequence. Reading it has no side
294    /// effects and never performs I/O.
295    pub fn esc_timeout(&self) -> Duration {
296        self.esc_timeout
297    }
298
299    /// Set the escape-sequence timeout.
300    ///
301    /// `timeout` is how long the source waits for a continuation byte before a
302    /// buffered partial escape sequence is force-resolved. The default is
303    /// [`DEFAULT_ESC_TIMEOUT`]. A zero duration makes ambiguous prefixes expire
304    /// on the next poll cycle.
305    ///
306    /// This is a consuming builder intended to be chained after
307    /// [`EventSource::new`]. It does not inspect or clear any already-buffered
308    /// input and never panics.
309    pub fn with_esc_timeout(mut self, timeout: Duration) -> Self {
310        self.esc_timeout = timeout;
311        self
312    }
313
314    /// Set the idle timeout for an open bracketed paste.
315    ///
316    /// If no further input arrives before `timeout`, the source flushes any
317    /// held bytes as a final [`Event::PasteChunk`], synthesizes
318    /// [`Event::PasteEnd`], and leaves paste mode. Passing `None` disables this
319    /// safety net and waits indefinitely for a real terminator. The default is
320    /// `Some(`[`DEFAULT_PASTE_IDLE_TIMEOUT`]`)`.
321    ///
322    /// This is a consuming builder intended to be chained after
323    /// [`EventSource::new`]. It never panics.
324    pub fn with_paste_idle_timeout(mut self, timeout: Option<Duration>) -> Self {
325        self.paste_idle_timeout = timeout;
326        self
327    }
328
329    /// Return a cloneable [`Waker`] bound to this source.
330    ///
331    /// Use the returned handle from another thread to interrupt a blocking
332    /// [`EventSource::poll`] or [`EventSource::read`]. Cloning the waker does
333    /// not clone the source or its input handle.
334    pub fn waker(&self) -> Waker {
335        self.waker.clone()
336    }
337
338    /// Return a clone of the shared [`Poller`] handle.
339    ///
340    /// The poller is `Arc`-wrapped so a waiter thread can block on readiness
341    /// without holding the source mutex. Both the underlying epoll and kqueue
342    /// registrations are level-triggered, so a concurrent poll from a waiter
343    /// and a subsequent drain from the owner both observe the same readiness.
344    #[cfg(feature = "async")]
345    pub(super) fn poller(&self) -> Arc<dyn Poller> {
346        Arc::clone(&self.poller)
347    }
348
349    /// Return whether out-of-band resize handling is enabled.
350    ///
351    /// On Unix, `true` means a readable SIGWINCH pipe can produce
352    /// [`Event::Resize`]. On Windows, resize records are delivered in-band and
353    /// this flag has no practical effect.
354    pub fn handle_resize(&self) -> bool {
355        self.handle_resize
356    }
357
358    /// Control whether the source delivers [`Event::Resize`] from the
359    /// kernel's out-of-band window-resize notification (`SIGWINCH` on
360    /// Unix). Defaults to `true`.
361    ///
362    /// Set this to `false` after enabling in-band resize reports (DEC
363    /// mode 2048): the terminal then reports size changes in-band as
364    /// `CSI 48 t`, which the decoder surfaces as [`Event::Resize`], so
365    /// leaving the `SIGWINCH` path on would deliver each resize twice.
366    /// Restore it to `true` when in-band reporting is disabled again.
367    ///
368    /// No effect on Windows, where resize is always delivered in-band
369    /// through the decoder.
370    pub fn set_handle_resize(&mut self, enable: bool) {
371        self.handle_resize = enable;
372    }
373
374    /// Wait up to `timeout` for at least one event to become available.
375    ///
376    /// This method performs I/O, drains decoder output into the internal queue,
377    /// handles resize notifications, and resolves any expired ESC or paste
378    /// deadlines. `None` means block until an event or wake; `Some(Duration::ZERO)`
379    /// means perform a non-blocking readiness pass.
380    ///
381    /// Returns:
382    ///
383    /// * `Ok(true)` when the queue has at least one event;
384    /// * `Ok(false)` when the timeout elapsed or a paired [`Waker`] interrupted
385    ///   the wait without producing an event;
386    /// * `Err(_)` for fatal input or platform readiness errors.
387    pub fn poll(&mut self, timeout: Option<Duration>) -> io::Result<bool> {
388        if !self.queue.is_empty() {
389            return Ok(true);
390        }
391        let deadline = timeout.map(|t| Instant::now() + t);
392        loop {
393            let remaining = deadline.map(|d| d.saturating_duration_since(Instant::now()));
394            match self.fill(remaining) {
395                Ok(()) => {
396                    if !self.queue.is_empty() {
397                        return Ok(true);
398                    }
399                    if let Some(left) = remaining
400                        && left.is_zero()
401                    {
402                        return Ok(false);
403                    }
404                }
405                Err(e) if e.kind() == io::ErrorKind::Interrupted => return Ok(false),
406                Err(e) => return Err(e),
407            }
408        }
409    }
410
411    /// Return the next queued event without performing I/O.
412    ///
413    /// This only pops the internal queue. Call [`EventSource::poll`] first when
414    /// the queue may be empty but input could be ready. Returns `None` when no
415    /// event is currently queued.
416    pub fn try_read(&mut self) -> Option<Event> {
417        self.queue.pop_front()
418    }
419
420    /// One read-decode cycle: resolve any overdue decode deadline, wait up
421    /// to `timeout` for readiness, then service it. Winch surfaces a
422    /// [`Event::Resize`]; a [`Waker`] surfaces `Err(Interrupted)` without
423    /// touching decode state; ready input is drained and decoded; a wait
424    /// that returns with no input ready resolves the decode deadline it was
425    /// tightened to. Fills [`Self::queue`].
426    pub(super) fn fill(&mut self, timeout: Option<Duration>) -> io::Result<()> {
427        // Resolve an already-overdue deadline before reading, so a late
428        // continuation byte cannot merge with a sequence that has expired.
429        self.expire_elapsed();
430        if !self.queue.is_empty() {
431            return Ok(());
432        }
433
434        let effective = self.effective_timeout(timeout);
435        let mut ready = [false; READY_SLOTS];
436        self.poller.poll(&mut ready, effective)?;
437
438        #[cfg(unix)]
439        if ready[READY_WINCH] {
440            self.handle_winch();
441        }
442
443        if ready[READY_WAKE] {
444            self.drain_wake();
445            return Err(io::Error::new(io::ErrorKind::Interrupted, "wake"));
446        }
447
448        if ready[READY_INPUT] {
449            self.drain_input()?;
450        } else {
451            // The wait was tightened to a decode deadline that has elapsed.
452            self.expire_elapsed();
453        }
454        Ok(())
455    }
456
457    /// Block until the next event is available, then return it.
458    ///
459    /// This repeatedly checks the queue and calls [`EventSource::poll`] with no
460    /// caller timeout. It returns the next [`Event`] on success.
461    ///
462    /// Returns [`io::ErrorKind::Interrupted`] if a paired [`Waker`] fired while
463    /// waiting, and propagates fatal input/readiness errors.
464    pub fn read(&mut self) -> io::Result<Event> {
465        loop {
466            if let Some(ev) = self.queue.pop_front() {
467                return Ok(ev);
468            }
469            if !self.poll(None)? {
470                return Err(io::Error::new(io::ErrorKind::Interrupted, "wake"));
471            }
472        }
473    }
474
475    /// Return an event to the front of the queue, so the next
476    /// [`read`](Self::read) / [`try_read`](Self::try_read) yields it before
477    /// anything already queued. Use to put back an event read while waiting
478    /// for a specific reply (e.g. a cursor-position report), preserving it
479    /// for normal delivery. Restore a batch in original order by unreading
480    /// in reverse.
481    pub fn unread(&mut self, event: Event) {
482        self.queue.push_front(event);
483    }
484
485    /// Push a freshly produced event onto the queue.
486    pub(super) fn emit(&mut self, ev: Event) {
487        self.queue.push_back(ev);
488    }
489
490    /// Drive the parser as far as it will go against the bytes
491    /// currently in `pending`, pushing extracted events onto the
492    /// queue and arming the appropriate timeout deadline.
493    ///
494    /// While the decoder is in bracketed paste, the paste-idle
495    /// deadline governs (and is reset on every drain, since drain is
496    /// only called after fresh input arrived). Otherwise the ESC
497    /// disambiguation deadline arms when a partial sequence sits at
498    /// the head of `pending`.
499    pub(super) fn drain_parser(&mut self) {
500        loop {
501            let (n, ev) = self.parser.parse_one(self.pending.slice());
502            if n == 0 && ev.is_none() {
503                break;
504            }
505            if n > 0 {
506                self.pending.consume(n);
507            }
508            if let Some(ev) = ev {
509                self.emit(ev);
510            }
511        }
512
513        if self.parser.in_paste() {
514            // In paste: only the paste-idle timer applies. Reset on
515            // every drain (input has just arrived).
516            self.esc_deadline = None;
517            self.paste_deadline = self.paste_idle_timeout.map(|t| Instant::now() + t);
518            return;
519        }
520
521        // Not in paste: clear paste deadline; arm esc deadline if a
522        // partial sequence sits at the head.
523        self.paste_deadline = None;
524        let Some(b0) = self.pending.first() else {
525            self.esc_deadline = None;
526            return;
527        };
528        let armable = b0 == 0x1b || is_c1_introducer(b0);
529        if armable {
530            if self.esc_deadline.is_none() {
531                self.esc_deadline = Some(Instant::now() + self.esc_timeout);
532            }
533        } else {
534            self.esc_deadline = None;
535        }
536    }
537
538    /// Force-resolve a buffered partial sequence whose ESC deadline
539    /// elapsed.
540    pub(super) fn expire_partial(&mut self) {
541        self.esc_deadline = None;
542        if self.pending.first().is_none() {
543            return;
544        }
545        // Flip the decoder into expired mode so its recursive ESC
546        // handler can commit buffered partial sequences (e.g. resolving
547        // `\x1b\x1b` to `Alt+Esc`). Anything the decoder still can't
548        // consume falls back to the single-byte fallback below.
549        self.parser.set_expired(true);
550        self.drain_parser();
551        while let Some(b0) = self.pending.first() {
552            let ev = self
553                .parser
554                .expire_leading(b0)
555                .unwrap_or_else(|| Event::Unknown(vec![b0]));
556            self.pending.consume(1);
557            self.emit(ev);
558            self.drain_parser();
559        }
560        self.parser.set_expired(false);
561    }
562
563    /// Force-close a stuck bracketed paste. Flushes any leftover
564    /// pending bytes as a final `PasteChunk`, then enqueues
565    /// `PasteEnd` and clears the decoder's paste state.
566    ///
567    /// Called from `pump` when the paste-idle deadline elapses, and
568    /// from the public [`EventSource::end_paste`] escape hatch.
569    pub(super) fn expire_paste(&mut self) {
570        self.paste_deadline = None;
571        if !self.parser.in_paste() {
572            return;
573        }
574        if !self.pending.is_empty() {
575            let bytes = self.pending.slice().to_vec();
576            self.pending.clear();
577            self.emit(Event::PasteChunk(bytes));
578        }
579        if let Some(ev) = self.parser.end_paste() {
580            self.emit(ev);
581        }
582    }
583
584    /// Force-exit bracketed paste mode.
585    ///
586    /// If the decoder is currently inside a paste, this flushes any pending
587    /// bytes as a [`Event::PasteChunk`], queues [`Event::PasteEnd`], and clears
588    /// paste state so subsequent bytes parse as ordinary input. Use it when an
589    /// embedding application enforces a paste size cap, user cancellation, or a
590    /// custom watchdog. It has no effect outside paste mode and never panics.
591    pub fn end_paste(&mut self) {
592        self.expire_paste();
593    }
594
595    /// Resolve any decode deadline that has already elapsed, before more
596    /// bytes are read. A buffered partial `ESC` past its window becomes a
597    /// bare `Esc`; an idle bracketed paste past its window is force-closed.
598    /// Run before draining input so a late continuation byte can't merge
599    /// with a sequence whose deadline already passed, and again after a
600    /// wait that returned no input. A no-op when no deadline is overdue;
601    /// the two cases are mutually exclusive (either mid-paste or holding a
602    /// partial escape, never both).
603    pub(super) fn expire_elapsed(&mut self) {
604        let now = Instant::now();
605        if self.parser.in_paste() {
606            if self.paste_deadline.is_some_and(|d| d <= now) {
607                self.expire_paste();
608            }
609        } else if self.esc_deadline.is_some_and(|d| d <= now) {
610            self.expire_partial();
611        }
612    }
613
614    /// Effective wait for the next readiness poll: the caller's `timeout`
615    /// tightened to the nearest decode deadline (ESC or paste-idle,
616    /// whichever is sooner) so a buffered partial sequence resolves
617    /// promptly even when the caller asked to block longer.
618    pub(super) fn effective_timeout(&self, timeout: Option<Duration>) -> Option<Duration> {
619        let now = Instant::now();
620        let deadline = match (self.esc_deadline, self.paste_deadline) {
621            (Some(a), Some(b)) => Some(a.min(b)),
622            (a, b) => a.or(b),
623        };
624        let internal = deadline.map(|d| d.saturating_duration_since(now));
625        match (timeout, internal) {
626            (Some(t), Some(i)) => Some(t.min(i)),
627            (None, i) => i,
628            (t, None) => t,
629        }
630    }
631}