Skip to main content

uncurses/event/
source_unix.rs

1//! Unix-specific construction and readiness servicing for [`EventSource`].
2//!
3//! ## Purpose
4//!
5//! This module supplies the Unix [`EventSource::new`] implementation and the
6//! platform hooks used by the shared source pump: drain the wake self-pipe, read
7//! ready bytes, and surface `SIGWINCH` as [`Event::Resize`].
8//!
9//! ```text
10//! [input fd] ─┐
11//! [wake pipe] ├─▶ Poller ──▶ EventSource::fill
12//! [winch pipe]┘        ├─ wake: Interrupted
13//!                      ├─ input: read + decode
14//!                      └─ winch: query Winsize + Resize
15//! ```
16//!
17//! ## Key types
18//!
19//! `UnixWakerInner` owns *both* ends of a non-blocking self-pipe. The shared
20//! [`Waker`] wraps it so other threads can interrupt blocking reads without
21//! touching decoder state. Both ends live behind the same `Arc` because a
22//! `Waker` may outlive its source, and writing to a pipe whose reader has
23//! closed raises a synchronous `SIGPIPE`.
24//!
25//! ## Gotchas
26//!
27//! The input fd is also used for `TIOCGWINSZ`, so it should refer to the same
28//! terminal whose size the caller wants. When in-band resize reports are
29//! enabled, [`EventSource::set_handle_resize`] should disable the SIGWINCH path
30//! to avoid duplicate resize events.
31#![cfg(unix)]
32
33use std::collections::VecDeque;
34use std::io;
35use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
36use std::sync::Arc;
37
38use super::decode::{Decoder, DecoderFlags};
39use super::pending::Pending;
40use super::poll::PollFd;
41use super::sigwinch as winch;
42use super::source::{
43    DEFAULT_BUFFER_CAPACITY, DEFAULT_ESC_TIMEOUT, DEFAULT_PASTE_IDLE_TIMEOUT, EventSource, Input,
44    Waker,
45};
46use crate::event::Event;
47use crate::terminal::get_window_size;
48
49pub(super) struct UnixWakerInner {
50    /// Write end of the self-pipe. Non-blocking; closed on drop.
51    tx: OwnedFd,
52    /// Read end, kept here rather than in [`EventSource`] so that it cannot be
53    /// closed while a `Waker` clone still holds `tx`. A write to a pipe whose
54    /// reader is gone raises a *synchronous* `SIGPIPE` on the calling thread,
55    /// which no error handling can intercept — invisible in a Rust binary
56    /// (std sets `SIG_IGN` at startup) but fatal inside a C or Go host.
57    /// `Waker` is public, `Clone` and `Send`, so it legitimately outlives the
58    /// source it came from; sharing both ends behind the same `Arc` makes that
59    /// safe instead of merely unlikely.
60    rx: OwnedFd,
61}
62
63impl UnixWakerInner {
64    pub(super) fn read_fd(&self) -> std::os::fd::RawFd {
65        self.rx.as_raw_fd()
66    }
67
68    pub(super) fn wake(&self) -> io::Result<()> {
69        let buf = b"w";
70        loop {
71            let n = unsafe { libc::write(self.tx.as_raw_fd(), buf.as_ptr() as *const _, 1) };
72            if n < 0 {
73                let err = io::Error::last_os_error();
74                match err.kind() {
75                    io::ErrorKind::Interrupted => continue,
76                    // Pipe full — an earlier wake byte is already pending,
77                    // which is all the consumer needs.
78                    io::ErrorKind::WouldBlock => return Ok(()),
79                    _ => return Err(err),
80                }
81            }
82            return Ok(());
83        }
84    }
85}
86
87// ---------------------------------------------------------------------------
88// Unix implementation
89// ---------------------------------------------------------------------------
90
91impl<I> EventSource<I>
92where
93    I: Input,
94{
95    /// Build a new Unix event source for `input`.
96    ///
97    /// The handle is used both for byte reads and as the `TIOCGWINSZ` target
98    /// when `SIGWINCH` fires, so it should refer to the terminal whose size the
99    /// caller cares about. Construction creates two non-blocking self-pipes,
100    /// subscribes to the shared SIGWINCH fan-out, and registers the input, wake,
101    /// and resize fds with the selected poll backend.
102    ///
103    /// Timeouts start at [`DEFAULT_ESC_TIMEOUT`] and
104    /// [`DEFAULT_PASTE_IDLE_TIMEOUT`]; override them with
105    /// [`EventSource::with_esc_timeout`] and
106    /// [`EventSource::with_paste_idle_timeout`].
107    ///
108    /// Resize deduplication starts from the size `input` reports here, so a
109    /// `SIGWINCH` that does not actually change the size produces no
110    /// [`Event::Resize`].
111    ///
112    /// Returns any OS error from pipe creation, fd configuration, SIGWINCH
113    /// subscription, or poller construction. It does not read from `input`.
114    pub fn new(input: I) -> io::Result<Self> {
115        let (pipe_rx, pipe_tx) = make_self_pipe()?;
116        let winch_sub = winch::subscribe()?;
117        let waker = Waker::from_unix_inner(Arc::new(UnixWakerInner {
118            tx: pipe_tx,
119            rx: pipe_rx,
120        }));
121
122        // Watch input, wake pipe, and winch pipe — in the fixed index
123        // order the pump/ingest path relies on. Detect a tty input fd up
124        // front so Darwin can pick the select backend (its kqueue spins
125        // on tty character devices).
126        let input_fd = input.as_fd().as_raw_fd();
127        let input_is_tty = unsafe { libc::isatty(input_fd) } == 1;
128        // Seed the resize dedupe with the size we start at, so the first wake
129        // reports a resize only if one actually happened. Without this any
130        // first wake emits, including one caused by a late handler write that
131        // landed in this slot's pooled pipe just after it was leased to us.
132        // `None` on a non-tty, where there is no size to compare against.
133        let last_size = get_window_size(input.as_fd()).ok();
134        let fds: [PollFd; 3] = [input_fd, waker.pipe_read_fd(), winch_sub.read_fd()];
135        let poller = super::poll::new_poller(&fds, input_is_tty)?;
136
137        Ok(Self {
138            input,
139            parser: Decoder::new(DecoderFlags::empty()),
140            pending: Pending::with_capacity(DEFAULT_BUFFER_CAPACITY),
141            esc_timeout: DEFAULT_ESC_TIMEOUT,
142            esc_deadline: None,
143            paste_idle_timeout: Some(DEFAULT_PASTE_IDLE_TIMEOUT),
144            paste_deadline: None,
145            queue: VecDeque::with_capacity(16),
146            waker,
147            handle_resize: true,
148            poller,
149            winch_sub,
150            last_size,
151        })
152    }
153
154    /// Drain pending wake bytes after a [`Waker`] fired.
155    ///
156    /// Platform hook for [`EventSource::fill`]. Multiple wake bytes may have
157    /// coalesced; draining them all lets a subsequent poll block again.
158    pub(super) fn drain_wake(&mut self) {
159        drain_pipe(self.waker.pipe_read_fd());
160    }
161
162    /// Read ready input bytes and run them through the decoder.
163    ///
164    /// Platform hook for [`EventSource::fill`]. `Interrupted` and `WouldBlock`
165    /// reads are treated as a transient absence of bytes. A zero-length read is
166    /// surfaced as [`io::ErrorKind::UnexpectedEof`]. If the pending buffer is
167    /// already full, it is cleared because its capacity is the hard cap on one
168    /// undecoded sequence.
169    pub(super) fn drain_input(&mut self) -> io::Result<()> {
170        // If the buffer is full and the parser still couldn't extract
171        // an event, the contract says the buffer size is the hard cap
172        // on any single sequence — drop the buffer silently and resume.
173        if self.pending.is_full() {
174            self.pending.clear();
175            self.esc_deadline = None;
176        }
177        let n = match self.input.read(self.pending.spare_mut()) {
178            Ok(n) => n,
179            Err(e) => {
180                if matches!(
181                    e.kind(),
182                    io::ErrorKind::Interrupted | io::ErrorKind::WouldBlock
183                ) {
184                    return Ok(());
185                }
186                return Err(e);
187            }
188        };
189        if n == 0 {
190            return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "input closed"));
191        }
192        self.pending.advance_written(n);
193        #[cfg(debug_assertions)]
194        {
195            let s = self.pending.slice();
196            crate::trace::tee_input(&s[s.len() - n..]);
197        }
198        self.drain_parser();
199        Ok(())
200    }
201
202    pub(super) fn handle_winch(&mut self) {
203        drain_pipe(self.winch_sub.read_fd());
204        // When in-band resize reporting is enabled the host disables
205        // this path; the terminal delivers resizes through the decoder
206        // instead, so emitting here too would duplicate every event.
207        if !self.handle_resize {
208            return;
209        }
210        let new_size = match get_window_size(self.input.as_fd()) {
211            Ok(sz) => sz,
212            Err(_) => return,
213        };
214        if Some(new_size) == self.last_size {
215            return;
216        }
217        self.last_size = Some(new_size);
218        self.emit(Event::Resize(new_size));
219    }
220}
221
222pub(super) fn make_self_pipe() -> io::Result<(OwnedFd, OwnedFd)> {
223    let mut fds = [0i32; 2];
224    let rc = unsafe { libc::pipe(fds.as_mut_ptr()) };
225    if rc != 0 {
226        return Err(io::Error::last_os_error());
227    }
228    // SAFETY: pipe(2) just produced two fresh, owned fds.
229    let rx = unsafe { OwnedFd::from_raw_fd(fds[0]) };
230    let tx = unsafe { OwnedFd::from_raw_fd(fds[1]) };
231    set_nonblock_cloexec(rx.as_raw_fd())?;
232    set_nonblock_cloexec(tx.as_raw_fd())?;
233    Ok((rx, tx))
234}
235
236fn set_nonblock_cloexec(fd: i32) -> io::Result<()> {
237    unsafe {
238        let flags = libc::fcntl(fd, libc::F_GETFL);
239        if flags < 0 {
240            return Err(io::Error::last_os_error());
241        }
242        if libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) < 0 {
243            return Err(io::Error::last_os_error());
244        }
245        let fd_flags = libc::fcntl(fd, libc::F_GETFD);
246        if fd_flags < 0 {
247            return Err(io::Error::last_os_error());
248        }
249        if libc::fcntl(fd, libc::F_SETFD, fd_flags | libc::FD_CLOEXEC) < 0 {
250            return Err(io::Error::last_os_error());
251        }
252    }
253    Ok(())
254}
255
256pub(super) fn drain_pipe(fd: i32) {
257    let mut buf = [0u8; 32];
258    loop {
259        let n = unsafe { libc::read(fd, buf.as_mut_ptr() as *mut _, buf.len()) };
260        if n <= 0 {
261            break;
262        }
263    }
264}
265
266#[cfg(test)]
267mod tests {
268    use super::*;
269    use crate::event::KeyCode;
270    use std::fs::File;
271    use std::os::fd::FromRawFd;
272    use std::thread;
273    use std::time::Duration;
274    use std::time::Instant;
275
276    fn make_pipe() -> (File, File) {
277        let mut fds = [0i32; 2];
278        let rc = unsafe { libc::pipe(fds.as_mut_ptr()) };
279        assert_eq!(rc, 0, "pipe() failed");
280        // SAFETY: pipe(2) produced two fresh, owned fds.
281        let rx = unsafe { File::from_raw_fd(fds[0]) };
282        let tx = unsafe { File::from_raw_fd(fds[1]) };
283        (rx, tx)
284    }
285
286    fn write_byte(f: &File, byte: u8) {
287        let n = unsafe { libc::write(f.as_raw_fd(), &byte as *const _ as *const _, 1) };
288        assert_eq!(n, 1);
289    }
290
291    fn write_bytes(f: &File, bytes: &[u8]) {
292        let n = unsafe { libc::write(f.as_raw_fd(), bytes.as_ptr() as *const _, bytes.len()) };
293        assert_eq!(n, bytes.len() as isize);
294    }
295
296    fn new_reader(input: File) -> EventSource<File> {
297        EventSource::new(input)
298            .unwrap()
299            .with_esc_timeout(Duration::from_millis(50))
300    }
301
302    #[test]
303    fn reads_event_from_input_fd() {
304        let (rx, tx) = make_pipe();
305        let mut src = new_reader(rx);
306        write_byte(&tx, b'a');
307        assert!(src.poll(Some(Duration::from_secs(1))).unwrap());
308        let ev = src.read().unwrap();
309        match ev {
310            Event::KeyPress(k) => assert_eq!(k.code, KeyCode::Char('a')),
311            other => panic!("unexpected event {:?}", other),
312        }
313    }
314
315    #[test]
316    fn cursor_reports_and_f3_preserve_event_order() {
317        use crate::event::{Key, KeyModifiers};
318        use crate::layout::Position;
319
320        let cpr = |x, y, page| Event::CursorPosition {
321            position: Position::new(x, y),
322            page,
323        };
324        let f3 = |modifiers| Event::KeyPress(Key::new(KeyCode::F(3), modifiers));
325        for prefix in [b"\x1b[".as_slice(), b"\x9b".as_slice()] {
326            for (body, expected) in [
327                ("R", Some(vec![f3(KeyModifiers::empty())])),
328                ("1R", Some(vec![f3(KeyModifiers::empty())])),
329                ("1;5R", Some(vec![f3(KeyModifiers::CTRL), cpr(4, 0, 0)])),
330                ("5;10R", Some(vec![cpr(9, 4, 0)])),
331                ("1;17R", Some(vec![cpr(16, 0, 0)])),
332                ("?1;5R", Some(vec![cpr(4, 0, 0)])),
333                ("?5;10;2R", Some(vec![cpr(9, 4, 2)])),
334                ("?5;10;R", Some(vec![cpr(9, 4, 0)])),
335                ("?1;5;2;3R", Some(vec![cpr(4, 0, 2)])),
336                ("1;2;3R", None),
337                ("?R", None),
338                (">1;5R", None),
339                ("?1;5 R", None),
340            ] {
341                let mut wire = [prefix, body.as_bytes()].concat();
342                let mut expected =
343                    expected.unwrap_or_else(|| vec![Event::UnknownCsi(wire.clone())]);
344                wire.push(b'x');
345                expected.push(Event::KeyPress(Key::new(
346                    KeyCode::Char('x'),
347                    KeyModifiers::empty(),
348                )));
349                let (rx, tx) = make_pipe();
350                let mut src = new_reader(rx);
351                write_bytes(&tx, &wire);
352                for event in expected {
353                    assert!(src.poll(Some(Duration::from_secs(1))).unwrap(), "{wire:?}");
354                    assert_eq!(src.read().unwrap(), event, "{wire:?}");
355                }
356                assert!(src.try_read().is_none(), "{wire:?}");
357            }
358        }
359    }
360
361    #[test]
362    fn timeout_returns_none() {
363        let (rx, _tx) = make_pipe();
364        let mut src = new_reader(rx);
365        let start = Instant::now();
366        let res = src.poll(Some(Duration::from_millis(10))).unwrap();
367        let elapsed = start.elapsed();
368        assert!(!res);
369        assert!(elapsed >= Duration::from_millis(5));
370    }
371
372    #[test]
373    fn waker_interrupts_blocking_read() {
374        let (rx, _tx) = make_pipe();
375        let mut src = new_reader(rx);
376        let waker = src.waker();
377        let handle = thread::spawn(move || {
378            thread::sleep(Duration::from_millis(20));
379            waker.wake().unwrap();
380        });
381        let err = src.read().expect_err("should be Interrupted");
382        handle.join().unwrap();
383        assert_eq!(err.kind(), io::ErrorKind::Interrupted);
384    }
385
386    #[test]
387    fn esc_resolves_before_late_continuation_byte() {
388        // A buffered partial ESC whose deadline elapses must resolve to a
389        // bare Esc before a continuation byte that arrives afterward is
390        // read, so the two never merge into an Alt-modified key.
391        let (rx, tx) = make_pipe();
392        let mut src = new_reader(rx); // 50ms esc timeout
393        write_bytes(&tx, b"\x1b");
394        // Drain the lone ESC so its disambiguation deadline is armed; the
395        // queue stays empty because the sequence is still partial.
396        assert!(!src.poll(Some(Duration::from_millis(0))).unwrap());
397        // Let the deadline elapse without draining, then deliver a byte
398        // that would otherwise complete an ESC-prefixed sequence.
399        thread::sleep(Duration::from_millis(80));
400        write_bytes(&tx, b"a");
401        let first = src.read().unwrap();
402        assert!(
403            matches!(&first, Event::KeyPress(k) if k.code == KeyCode::Escape),
404            "expected bare Esc, got {:?}",
405            first
406        );
407        let second = src.read().unwrap();
408        assert!(
409            matches!(&second, Event::KeyPress(k) if k.code == KeyCode::Char('a')),
410            "expected 'a', got {:?}",
411            second
412        );
413    }
414
415    #[test]
416    fn paste_idle_timeout_synthesizes_paste_end() {
417        let (rx, tx) = make_pipe();
418        let mut src = EventSource::new(rx)
419            .unwrap()
420            .with_paste_idle_timeout(Some(Duration::from_millis(40)));
421        write_bytes(&tx, b"\x1b[200~hello");
422        // Drain PasteStart + initial chunk.
423        let mut got_start = false;
424        let mut got_chunk = false;
425        for _ in 0..4 {
426            if !src.poll(Some(Duration::from_millis(20))).unwrap() {
427                break;
428            }
429            while let Some(ev) = src.try_read() {
430                match ev {
431                    Event::PasteStart => got_start = true,
432                    Event::PasteChunk(b) => {
433                        assert_eq!(b, b"hello".to_vec());
434                        got_chunk = true;
435                    }
436                    other => panic!("unexpected pre-timeout event {:?}", other),
437                }
438            }
439            if got_start && got_chunk {
440                break;
441            }
442        }
443        assert!(got_start && got_chunk);
444
445        // Now stop sending data and wait past the paste-idle deadline.
446        let start = Instant::now();
447        assert!(src.poll(Some(Duration::from_secs(5))).unwrap());
448        let ev = src.read().unwrap();
449        let elapsed = start.elapsed();
450        assert_eq!(ev, Event::PasteEnd);
451        assert!(
452            elapsed < Duration::from_millis(500),
453            "elapsed = {:?}",
454            elapsed
455        );
456    }
457
458    #[test]
459    fn paste_completes_when_terminator_arrives_within_idle_window() {
460        let (rx, tx) = make_pipe();
461        let mut src = EventSource::new(rx)
462            .unwrap()
463            .with_paste_idle_timeout(Some(Duration::from_millis(500)));
464        write_bytes(&tx, b"\x1b[200~hi");
465        let _ = src.poll(Some(Duration::from_millis(50))).unwrap();
466        while src.try_read().is_some() {}
467        // Sub-timeout pause, then deliver the terminator.
468        thread::sleep(Duration::from_millis(50));
469        write_bytes(&tx, b"\x1b[201~");
470        assert!(src.poll(Some(Duration::from_secs(1))).unwrap());
471        let mut saw_end = false;
472        while let Some(ev) = src.try_read() {
473            if matches!(ev, Event::PasteEnd) {
474                saw_end = true;
475            }
476        }
477        if !saw_end {
478            // Drain another pump cycle if necessary.
479            let _ = src.poll(Some(Duration::from_millis(50))).unwrap();
480            while let Some(ev) = src.try_read() {
481                if matches!(ev, Event::PasteEnd) {
482                    saw_end = true;
483                }
484            }
485        }
486        assert!(saw_end, "expected PasteEnd within the idle window");
487    }
488
489    #[test]
490    fn explicit_end_paste_recovers_stream() {
491        let (rx, tx) = make_pipe();
492        let mut src = EventSource::new(rx).unwrap().with_paste_idle_timeout(None);
493        write_bytes(&tx, b"\x1b[200~stuck");
494        let _ = src.poll(Some(Duration::from_millis(50))).unwrap();
495        while src.try_read().is_some() {}
496
497        // No terminator will arrive; force-exit.
498        src.end_paste();
499        let ev = src.try_read().expect("PasteEnd should be queued");
500        assert_eq!(ev, Event::PasteEnd);
501
502        // Subsequent bytes parse as normal input again.
503        write_bytes(&tx, b"a");
504        assert!(src.poll(Some(Duration::from_secs(1))).unwrap());
505        let ev = src.read().unwrap();
506        assert!(matches!(
507            ev,
508            Event::KeyPress(ref k) if k.code == KeyCode::Char('a')
509        ));
510    }
511
512    #[test]
513    fn paste_idle_timeout_disabled_blocks_indefinitely() {
514        let (rx, tx) = make_pipe();
515        let mut src = EventSource::new(rx).unwrap().with_paste_idle_timeout(None);
516        write_bytes(&tx, b"\x1b[200~partial");
517        let _ = src.poll(Some(Duration::from_millis(50))).unwrap();
518        while src.try_read().is_some() {}
519
520        // With the safety net disabled, a long-but-finite caller
521        // timeout should expire without synthesising PasteEnd.
522        let res = src.poll(Some(Duration::from_millis(80))).unwrap();
523        assert!(!res, "should time out, not synthesise PasteEnd");
524        assert!(src.try_read().is_none());
525    }
526
527    /// The flags reach the decoder, and what they change is which key a byte
528    /// is called. `0x0d` is `enter` by default and `ctrl+m` when a caller
529    /// asks for that reading.
530    #[test]
531    fn the_decoder_flags_a_caller_chose_are_the_ones_used() {
532        use crate::event::DecoderFlags;
533
534        let (rx, tx) = make_pipe();
535        let mut src = EventSource::new(rx).unwrap();
536        write_byte(&tx, 0x0d);
537        assert!(src.poll(Some(Duration::from_secs(1))).unwrap());
538        let Event::KeyPress(k) = src.read().unwrap() else {
539            panic!("expected a key")
540        };
541        assert_eq!(k.code, KeyCode::Enter, "the reading a terminal means");
542
543        let (rx, tx) = make_pipe();
544        let mut src = EventSource::new(rx)
545            .unwrap()
546            .with_decoder_flags(DecoderFlags::CTRL_M);
547        assert_eq!(src.decoder_flags(), DecoderFlags::CTRL_M);
548        write_byte(&tx, 0x0d);
549        assert!(src.poll(Some(Duration::from_secs(1))).unwrap());
550        let Event::KeyPress(k) = src.read().unwrap() else {
551            panic!("expected a key")
552        };
553        assert_eq!(k.code, KeyCode::Char('m'));
554        assert_eq!(k.modifiers, crate::event::KeyModifiers::CTRL);
555    }
556
557    /// `0x1b` is the Escape key and `ctrl+[` both, and an application that
558    /// asked for the Ctrl reading gets it for a real lone `ESC`: the one the
559    /// source resolves itself once the escape deadline passes, which is the
560    /// only way that byte ever arrives alone.
561    #[test]
562    fn a_lone_escape_from_the_terminal_follows_the_flag() {
563        use crate::event::DecoderFlags;
564
565        let (rx, tx) = make_pipe();
566        let mut src = EventSource::new(rx)
567            .unwrap()
568            .with_esc_timeout(Duration::from_millis(20))
569            .with_decoder_flags(DecoderFlags::CTRL_OPEN_BRACKET);
570        write_byte(&tx, 0x1b);
571        // The first poll buffers it; the deadline is what resolves it, so
572        // give the source long enough to reach its own.
573        let _ = src.poll(Some(Duration::from_millis(50))).unwrap();
574        assert!(src.poll(Some(Duration::from_secs(1))).unwrap());
575        let Event::KeyPress(k) = src.read().unwrap() else {
576            panic!("expected a key")
577        };
578        assert_eq!(k.code, KeyCode::Char('['));
579        assert_eq!(k.modifiers, crate::event::KeyModifiers::CTRL);
580    }
581
582    /// The same choice on a source already in use, which is the seam
583    /// [`Program::init_with`](crate::program::Program::init_with) reaches
584    /// through when it carries `ProgramOptions::legacy_keys`.
585    #[test]
586    fn the_flags_can_be_chosen_on_a_source_already_built() {
587        use crate::event::DecoderFlags;
588
589        let (rx, tx) = make_pipe();
590        let mut src = EventSource::new(rx).unwrap();
591        assert!(
592            src.decoder_flags().is_empty(),
593            "a source reads each as the named key until told otherwise"
594        );
595        src.set_decoder_flags(DecoderFlags::CTRL_I);
596        write_byte(&tx, 0x09);
597        assert!(src.poll(Some(Duration::from_secs(1))).unwrap());
598        let Event::KeyPress(k) = src.read().unwrap() else {
599            panic!("expected a key")
600        };
601        assert_eq!(k.code, KeyCode::Char('i'));
602        assert_eq!(k.modifiers, crate::event::KeyModifiers::CTRL);
603    }
604
605    #[test]
606    fn esc_deadline_does_not_fire_during_paste() {
607        // Pre-fix latent bug: while in paste, a partial ESC at the
608        // head of the pending buffer must not synthesise Key(Esc).
609        let (rx, tx) = make_pipe();
610        let mut src = EventSource::new(rx)
611            .unwrap()
612            .with_esc_timeout(Duration::from_millis(20))
613            .with_paste_idle_timeout(Some(Duration::from_secs(5)));
614        write_bytes(&tx, b"\x1b[200~body");
615        let _ = src.poll(Some(Duration::from_millis(50))).unwrap();
616        while src.try_read().is_some() {}
617
618        // Send only the beginning of the terminator: a partial ESC
619        // sequence at the head of pending. The esc_timeout (20 ms)
620        // must NOT fire — only the paste timeout (5 s) governs here.
621        write_bytes(&tx, b"\x1b[20");
622        let _ = src.poll(Some(Duration::from_millis(80))).unwrap();
623        let mut saw_esc = false;
624        while let Some(ev) = src.try_read() {
625            if matches!(ev, Event::KeyPress(ref k) if k.code == KeyCode::Escape) {
626                saw_esc = true;
627            }
628        }
629        assert!(!saw_esc, "esc deadline must not fire during paste");
630
631        // Complete the terminator: paste ends cleanly.
632        write_bytes(&tx, b"1~");
633        assert!(src.poll(Some(Duration::from_secs(1))).unwrap());
634        let mut saw_end = false;
635        while let Some(ev) = src.try_read() {
636            if matches!(ev, Event::PasteEnd) {
637                saw_end = true;
638            }
639        }
640        assert!(saw_end);
641    }
642
643    #[test]
644    fn esc_deadline_tightens_long_caller_timeout() {
645        let (rx, tx) = make_pipe();
646        let mut src = EventSource::new(rx)
647            .unwrap()
648            .with_esc_timeout(Duration::from_millis(20));
649        write_byte(&tx, 0x1b);
650        let _ = src.poll(Some(Duration::from_secs(60))).unwrap();
651        let start = Instant::now();
652        assert!(src.poll(Some(Duration::from_secs(60))).unwrap());
653        let ev = src.read().unwrap();
654        let elapsed = start.elapsed();
655        assert!(matches!(ev, Event::KeyPress(k) if k.code == KeyCode::Escape));
656        assert!(
657            elapsed < Duration::from_millis(500),
658            "elapsed = {:?}",
659            elapsed
660        );
661    }
662
663    #[test]
664    fn paste_end_after_chunk_is_delivered_without_extra_input() {
665        // Regression: when a paste body and its closing terminator arrive
666        // in the same read, the decoder returns the chunk first and queues
667        // PasteEnd on its internal pending list. The source must drain that
668        // queued event in the same drain pass — otherwise PasteEnd would
669        // stall until the next byte showed up.
670        let (rx, tx) = make_pipe();
671        let mut src = new_reader(rx);
672        write_bytes(&tx, b"\x1b[200~hello\x1b[201~");
673        assert!(src.poll(Some(Duration::from_secs(1))).unwrap());
674        assert!(matches!(src.read().unwrap(), Event::PasteStart));
675        assert!(matches!(src.read().unwrap(), Event::PasteChunk(ref b) if b == b"hello"));
676        assert!(matches!(src.read().unwrap(), Event::PasteEnd));
677    }
678
679    #[test]
680    fn handle_resize_false_suppresses_sigwinch_resize_event() {
681        // With resize handling disabled (the host has enabled in-band
682        // reports), a SIGWINCH must drain its wake pipe but surface no
683        // Event::Resize — the decoder delivers resizes in-band instead.
684        let stderr_fd = 2;
685        let ws: libc::winsize = unsafe { std::mem::zeroed() };
686        let probe = unsafe { libc::ioctl(stderr_fd, libc::TIOCGWINSZ, &ws as *const _) };
687        if probe < 0 {
688            return;
689        }
690        let stderr_dup = unsafe { libc::dup(stderr_fd) };
691        assert!(stderr_dup >= 0);
692        let stderr_file = unsafe { File::from_raw_fd(stderr_dup) };
693        let mut src = new_reader(stderr_file);
694        assert!(src.handle_resize());
695        src.set_handle_resize(false);
696        assert!(!src.handle_resize());
697        src.last_size = None;
698        unsafe { libc::raise(libc::SIGWINCH) };
699        // No event is produced; the poll runs to its (short) timeout.
700        assert!(!src.poll(Some(Duration::from_millis(50))).unwrap());
701        assert!(src.try_read().is_none());
702    }
703
704    /// The constructor seeds `last_size`, so a `SIGWINCH` that does not change
705    /// the size must not surface an event. That is what keeps a stray wake on a
706    /// recycled pool pipe from being mistaken for a resize.
707    #[cfg(not(target_os = "l4re"))]
708    #[test]
709    fn sigwinch_dedups_unchanged_size() {
710        let Some((master, _slave)) = crate::testutil::open_pty_pair() else {
711            return;
712        };
713        // Probe independently of the code under test: illumos ptys are STREAMS
714        // devices and a bare master does not answer TIOCGWINSZ. Skipping on the
715        // probe rather than on `last_size` keeps the assertion below meaningful
716        // everywhere the ioctl does work.
717        let ws: libc::winsize = unsafe { std::mem::zeroed() };
718        if unsafe { libc::ioctl(master.as_raw_fd(), libc::TIOCGWINSZ, &ws) } < 0 {
719            return;
720        }
721
722        let mut src = new_reader(master);
723        assert!(
724            src.last_size.is_some(),
725            "constructor did not seed the resize dedupe from the input fd"
726        );
727
728        unsafe { libc::raise(libc::SIGWINCH) };
729        assert!(
730            !src.poll(Some(Duration::from_millis(50))).unwrap(),
731            "an unchanged size surfaced a resize event"
732        );
733        assert!(src.try_read().is_none());
734    }
735
736    #[test]
737    fn sigwinch_surfaces_resize_event() {
738        // SIGWINCH requires a real tty to query TIOCGWINSZ on. Dup
739        // stderr — under cargo test it is typically a tty — and use
740        // that fd as the input source. If stderr isn't a tty, skip.
741        let stderr_fd = 2;
742        let ws: libc::winsize = unsafe { std::mem::zeroed() };
743        let probe = unsafe { libc::ioctl(stderr_fd, libc::TIOCGWINSZ, &ws as *const _) };
744        if probe < 0 {
745            return;
746        }
747        let stderr_dup = unsafe { libc::dup(stderr_fd) };
748        assert!(stderr_dup >= 0);
749        let stderr_file = unsafe { File::from_raw_fd(stderr_dup) };
750        let mut src = new_reader(stderr_file);
751        // Force a mismatched cached size so the SIGWINCH path surfaces
752        // the dedupe-suppressed event.
753        src.last_size = None;
754        unsafe { libc::raise(libc::SIGWINCH) };
755        assert!(src.poll(Some(Duration::from_secs(1))).unwrap());
756        let ev = src.read().unwrap();
757        assert!(matches!(ev, Event::Resize(_)));
758    }
759}