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 the write end of a non-blocking self-pipe. The
20//! shared [`Waker`] wraps it so other threads can interrupt
21//! blocking reads without touching decoder state.
22//!
23//! ## Gotchas
24//!
25//! The input fd is also used for `TIOCGWINSZ`, so it should refer to the same
26//! terminal whose size the caller wants. When in-band resize reports are
27//! enabled, [`EventSource::set_handle_resize`] should disable the SIGWINCH path
28//! to avoid duplicate resize events.
29#![cfg(unix)]
30
31use std::collections::VecDeque;
32use std::io;
33use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
34use std::sync::Arc;
35
36use super::decode::{Decoder, DecoderFlags};
37use super::pending::Pending;
38use super::poll::PollFd;
39use super::sigwinch as winch;
40use super::source::{
41    DEFAULT_BUFFER_CAPACITY, DEFAULT_ESC_TIMEOUT, DEFAULT_PASTE_IDLE_TIMEOUT, EventSource, Input,
42    Waker,
43};
44use crate::event::Event;
45use crate::terminal::get_window_size;
46
47pub(super) struct UnixWakerInner {
48    /// Write end of the self-pipe. Non-blocking; closed on drop.
49    tx: OwnedFd,
50}
51
52impl UnixWakerInner {
53    pub(super) fn wake(&self) -> io::Result<()> {
54        let buf = [b'w'];
55        loop {
56            let n = unsafe { libc::write(self.tx.as_raw_fd(), buf.as_ptr() as *const _, 1) };
57            if n < 0 {
58                let err = io::Error::last_os_error();
59                match err.kind() {
60                    io::ErrorKind::Interrupted => continue,
61                    // Pipe full — an earlier wake byte is already pending,
62                    // which is all the consumer needs.
63                    io::ErrorKind::WouldBlock => return Ok(()),
64                    _ => return Err(err),
65                }
66            }
67            return Ok(());
68        }
69    }
70}
71
72// ---------------------------------------------------------------------------
73// Unix implementation
74// ---------------------------------------------------------------------------
75
76impl<I> EventSource<I>
77where
78    I: Input,
79{
80    /// Build a new Unix event source for `input`.
81    ///
82    /// The handle is used both for byte reads and as the `TIOCGWINSZ` target
83    /// when `SIGWINCH` fires, so it should refer to the terminal whose size the
84    /// caller cares about. Construction creates two non-blocking self-pipes,
85    /// subscribes to the shared SIGWINCH fan-out, and registers the input, wake,
86    /// and resize fds with the selected poll backend.
87    ///
88    /// Timeouts start at [`DEFAULT_ESC_TIMEOUT`] and
89    /// [`DEFAULT_PASTE_IDLE_TIMEOUT`]; override them with
90    /// [`EventSource::with_esc_timeout`] and
91    /// [`EventSource::with_paste_idle_timeout`].
92    ///
93    /// Returns any OS error from pipe creation, fd configuration, SIGWINCH
94    /// subscription, or poller construction. It does not read from `input`.
95    pub fn new(input: I) -> io::Result<Self> {
96        let (pipe_rx, pipe_tx) = make_self_pipe()?;
97        let (winch_rx, winch_tx) = make_self_pipe()?;
98        let winch_sub = winch::subscribe(winch_tx.as_raw_fd())?;
99        let waker = Waker::from_unix_inner(Arc::new(UnixWakerInner { tx: pipe_tx }));
100
101        // Watch input, wake pipe, and winch pipe — in the fixed index
102        // order the pump/ingest path relies on. Detect a tty input fd up
103        // front so Darwin can pick the select backend (its kqueue spins
104        // on tty character devices).
105        let input_fd = input.as_fd().as_raw_fd();
106        let input_is_tty = unsafe { libc::isatty(input_fd) } == 1;
107        let fds: [PollFd; 3] = [input_fd, pipe_rx.as_raw_fd(), winch_rx.as_raw_fd()];
108        let poller = super::poll::new_poller(&fds, input_is_tty)?;
109
110        Ok(Self {
111            input,
112            parser: Decoder::new(DecoderFlags::empty()),
113            pending: Pending::with_capacity(DEFAULT_BUFFER_CAPACITY),
114            esc_timeout: DEFAULT_ESC_TIMEOUT,
115            esc_deadline: None,
116            paste_idle_timeout: Some(DEFAULT_PASTE_IDLE_TIMEOUT),
117            paste_deadline: None,
118            queue: VecDeque::with_capacity(16),
119            waker,
120            handle_resize: true,
121            poller,
122            pipe_rx,
123            winch_rx,
124            _winch_tx: winch_tx,
125            _winch_sub: winch_sub,
126            last_size: None,
127        })
128    }
129
130    /// Drain pending wake bytes after a [`Waker`] fired.
131    ///
132    /// Platform hook for [`EventSource::fill`]. Multiple wake bytes may have
133    /// coalesced; draining them all lets a subsequent poll block again.
134    pub(super) fn drain_wake(&mut self) {
135        drain_pipe(self.pipe_rx.as_raw_fd());
136    }
137
138    /// Read ready input bytes and run them through the decoder.
139    ///
140    /// Platform hook for [`EventSource::fill`]. `Interrupted` and `WouldBlock`
141    /// reads are treated as a transient absence of bytes. A zero-length read is
142    /// surfaced as [`io::ErrorKind::UnexpectedEof`]. If the pending buffer is
143    /// already full, it is cleared because its capacity is the hard cap on one
144    /// undecoded sequence.
145    pub(super) fn drain_input(&mut self) -> io::Result<()> {
146        // If the buffer is full and the parser still couldn't extract
147        // an event, the contract says the buffer size is the hard cap
148        // on any single sequence — drop the buffer silently and resume.
149        if self.pending.is_full() {
150            self.pending.clear();
151            self.esc_deadline = None;
152        }
153        let n = match self.input.read(self.pending.spare_mut()) {
154            Ok(n) => n,
155            Err(e) => {
156                if matches!(
157                    e.kind(),
158                    io::ErrorKind::Interrupted | io::ErrorKind::WouldBlock
159                ) {
160                    return Ok(());
161                }
162                return Err(e);
163            }
164        };
165        if n == 0 {
166            return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "input closed"));
167        }
168        self.pending.advance_written(n);
169        #[cfg(debug_assertions)]
170        {
171            let s = self.pending.slice();
172            crate::trace::tee_input(&s[s.len() - n..]);
173        }
174        self.drain_parser();
175        Ok(())
176    }
177
178    pub(super) fn handle_winch(&mut self) {
179        drain_pipe(self.winch_rx.as_raw_fd());
180        // When in-band resize reporting is enabled the host disables
181        // this path; the terminal delivers resizes through the decoder
182        // instead, so emitting here too would duplicate every event.
183        if !self.handle_resize {
184            return;
185        }
186        let new_size = match get_window_size(self.input.as_fd()) {
187            Ok(sz) => sz,
188            Err(_) => return,
189        };
190        if Some(new_size) == self.last_size {
191            return;
192        }
193        self.last_size = Some(new_size);
194        self.emit(Event::Resize(new_size));
195    }
196}
197
198fn make_self_pipe() -> io::Result<(OwnedFd, OwnedFd)> {
199    let mut fds = [0i32; 2];
200    let rc = unsafe { libc::pipe(fds.as_mut_ptr()) };
201    if rc != 0 {
202        return Err(io::Error::last_os_error());
203    }
204    // SAFETY: pipe(2) just produced two fresh, owned fds.
205    let rx = unsafe { OwnedFd::from_raw_fd(fds[0]) };
206    let tx = unsafe { OwnedFd::from_raw_fd(fds[1]) };
207    set_nonblock_cloexec(rx.as_raw_fd())?;
208    set_nonblock_cloexec(tx.as_raw_fd())?;
209    Ok((rx, tx))
210}
211
212fn set_nonblock_cloexec(fd: i32) -> io::Result<()> {
213    unsafe {
214        let flags = libc::fcntl(fd, libc::F_GETFL);
215        if flags < 0 {
216            return Err(io::Error::last_os_error());
217        }
218        if libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) < 0 {
219            return Err(io::Error::last_os_error());
220        }
221        let fd_flags = libc::fcntl(fd, libc::F_GETFD);
222        if fd_flags < 0 {
223            return Err(io::Error::last_os_error());
224        }
225        if libc::fcntl(fd, libc::F_SETFD, fd_flags | libc::FD_CLOEXEC) < 0 {
226            return Err(io::Error::last_os_error());
227        }
228    }
229    Ok(())
230}
231
232fn drain_pipe(fd: i32) {
233    let mut buf = [0u8; 32];
234    loop {
235        let n = unsafe { libc::read(fd, buf.as_mut_ptr() as *mut _, buf.len()) };
236        if n <= 0 {
237            break;
238        }
239    }
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245    use crate::event::KeyCode;
246    use std::fs::File;
247    use std::os::fd::FromRawFd;
248    use std::thread;
249    use std::time::Duration;
250    use std::time::Instant;
251
252    fn make_pipe() -> (File, File) {
253        let mut fds = [0i32; 2];
254        let rc = unsafe { libc::pipe(fds.as_mut_ptr()) };
255        assert_eq!(rc, 0, "pipe() failed");
256        // SAFETY: pipe(2) produced two fresh, owned fds.
257        let rx = unsafe { File::from_raw_fd(fds[0]) };
258        let tx = unsafe { File::from_raw_fd(fds[1]) };
259        (rx, tx)
260    }
261
262    fn write_byte(f: &File, byte: u8) {
263        let n = unsafe { libc::write(f.as_raw_fd(), &byte as *const _ as *const _, 1) };
264        assert_eq!(n, 1);
265    }
266
267    fn write_bytes(f: &File, bytes: &[u8]) {
268        let n = unsafe { libc::write(f.as_raw_fd(), bytes.as_ptr() as *const _, bytes.len()) };
269        assert_eq!(n, bytes.len() as isize);
270    }
271
272    fn new_reader(input: File) -> EventSource<File> {
273        EventSource::new(input)
274            .unwrap()
275            .with_esc_timeout(Duration::from_millis(50))
276    }
277
278    #[test]
279    fn reads_event_from_input_fd() {
280        let (rx, tx) = make_pipe();
281        let mut src = new_reader(rx);
282        write_byte(&tx, b'a');
283        assert!(src.poll(Some(Duration::from_secs(1))).unwrap());
284        let ev = src.read().unwrap();
285        match ev {
286            Event::KeyPress(k) => assert_eq!(k.code, KeyCode::Char('a')),
287            other => panic!("unexpected event {:?}", other),
288        }
289    }
290
291    #[test]
292    fn timeout_returns_none() {
293        let (rx, _tx) = make_pipe();
294        let mut src = new_reader(rx);
295        let start = Instant::now();
296        let res = src.poll(Some(Duration::from_millis(10))).unwrap();
297        let elapsed = start.elapsed();
298        assert!(!res);
299        assert!(elapsed >= Duration::from_millis(5));
300    }
301
302    #[test]
303    fn waker_interrupts_blocking_read() {
304        let (rx, _tx) = make_pipe();
305        let mut src = new_reader(rx);
306        let waker = src.waker();
307        let handle = thread::spawn(move || {
308            thread::sleep(Duration::from_millis(20));
309            waker.wake().unwrap();
310        });
311        let err = src.read().expect_err("should be Interrupted");
312        handle.join().unwrap();
313        assert_eq!(err.kind(), io::ErrorKind::Interrupted);
314    }
315
316    #[test]
317    fn esc_resolves_before_late_continuation_byte() {
318        // A buffered partial ESC whose deadline elapses must resolve to a
319        // bare Esc before a continuation byte that arrives afterward is
320        // read, so the two never merge into an Alt-modified key.
321        let (rx, tx) = make_pipe();
322        let mut src = new_reader(rx); // 50ms esc timeout
323        write_bytes(&tx, b"\x1b");
324        // Drain the lone ESC so its disambiguation deadline is armed; the
325        // queue stays empty because the sequence is still partial.
326        assert!(!src.poll(Some(Duration::from_millis(0))).unwrap());
327        // Let the deadline elapse without draining, then deliver a byte
328        // that would otherwise complete an ESC-prefixed sequence.
329        thread::sleep(Duration::from_millis(80));
330        write_bytes(&tx, b"a");
331        let first = src.read().unwrap();
332        assert!(
333            matches!(&first, Event::KeyPress(k) if k.code == KeyCode::Escape),
334            "expected bare Esc, got {:?}",
335            first
336        );
337        let second = src.read().unwrap();
338        assert!(
339            matches!(&second, Event::KeyPress(k) if k.code == KeyCode::Char('a')),
340            "expected 'a', got {:?}",
341            second
342        );
343    }
344
345    #[test]
346    fn paste_idle_timeout_synthesizes_paste_end() {
347        let (rx, tx) = make_pipe();
348        let mut src = EventSource::new(rx)
349            .unwrap()
350            .with_paste_idle_timeout(Some(Duration::from_millis(40)));
351        write_bytes(&tx, b"\x1b[200~hello");
352        // Drain PasteStart + initial chunk.
353        let mut got_start = false;
354        let mut got_chunk = false;
355        for _ in 0..4 {
356            if !src.poll(Some(Duration::from_millis(20))).unwrap() {
357                break;
358            }
359            while let Some(ev) = src.try_read() {
360                match ev {
361                    Event::PasteStart => got_start = true,
362                    Event::PasteChunk(b) => {
363                        assert_eq!(b, b"hello".to_vec());
364                        got_chunk = true;
365                    }
366                    other => panic!("unexpected pre-timeout event {:?}", other),
367                }
368            }
369            if got_start && got_chunk {
370                break;
371            }
372        }
373        assert!(got_start && got_chunk);
374
375        // Now stop sending data and wait past the paste-idle deadline.
376        let start = Instant::now();
377        assert!(src.poll(Some(Duration::from_secs(5))).unwrap());
378        let ev = src.read().unwrap();
379        let elapsed = start.elapsed();
380        assert_eq!(ev, Event::PasteEnd);
381        assert!(
382            elapsed < Duration::from_millis(500),
383            "elapsed = {:?}",
384            elapsed
385        );
386    }
387
388    #[test]
389    fn paste_completes_when_terminator_arrives_within_idle_window() {
390        let (rx, tx) = make_pipe();
391        let mut src = EventSource::new(rx)
392            .unwrap()
393            .with_paste_idle_timeout(Some(Duration::from_millis(500)));
394        write_bytes(&tx, b"\x1b[200~hi");
395        let _ = src.poll(Some(Duration::from_millis(50))).unwrap();
396        while src.try_read().is_some() {}
397        // Sub-timeout pause, then deliver the terminator.
398        thread::sleep(Duration::from_millis(50));
399        write_bytes(&tx, b"\x1b[201~");
400        assert!(src.poll(Some(Duration::from_secs(1))).unwrap());
401        let mut saw_end = false;
402        while let Some(ev) = src.try_read() {
403            if matches!(ev, Event::PasteEnd) {
404                saw_end = true;
405            }
406        }
407        if !saw_end {
408            // Drain another pump cycle if necessary.
409            let _ = src.poll(Some(Duration::from_millis(50))).unwrap();
410            while let Some(ev) = src.try_read() {
411                if matches!(ev, Event::PasteEnd) {
412                    saw_end = true;
413                }
414            }
415        }
416        assert!(saw_end, "expected PasteEnd within the idle window");
417    }
418
419    #[test]
420    fn explicit_end_paste_recovers_stream() {
421        let (rx, tx) = make_pipe();
422        let mut src = EventSource::new(rx).unwrap().with_paste_idle_timeout(None);
423        write_bytes(&tx, b"\x1b[200~stuck");
424        let _ = src.poll(Some(Duration::from_millis(50))).unwrap();
425        while src.try_read().is_some() {}
426
427        // No terminator will arrive; force-exit.
428        src.end_paste();
429        let ev = src.try_read().expect("PasteEnd should be queued");
430        assert_eq!(ev, Event::PasteEnd);
431
432        // Subsequent bytes parse as normal input again.
433        write_bytes(&tx, b"a");
434        assert!(src.poll(Some(Duration::from_secs(1))).unwrap());
435        let ev = src.read().unwrap();
436        assert!(matches!(
437            ev,
438            Event::KeyPress(ref k) if k.code == KeyCode::Char('a')
439        ));
440    }
441
442    #[test]
443    fn paste_idle_timeout_disabled_blocks_indefinitely() {
444        let (rx, tx) = make_pipe();
445        let mut src = EventSource::new(rx).unwrap().with_paste_idle_timeout(None);
446        write_bytes(&tx, b"\x1b[200~partial");
447        let _ = src.poll(Some(Duration::from_millis(50))).unwrap();
448        while src.try_read().is_some() {}
449
450        // With the safety net disabled, a long-but-finite caller
451        // timeout should expire without synthesising PasteEnd.
452        let res = src.poll(Some(Duration::from_millis(80))).unwrap();
453        assert!(!res, "should time out, not synthesise PasteEnd");
454        assert!(src.try_read().is_none());
455    }
456
457    #[test]
458    fn esc_deadline_does_not_fire_during_paste() {
459        // Pre-fix latent bug: while in paste, a partial ESC at the
460        // head of the pending buffer must not synthesise Key(Esc).
461        let (rx, tx) = make_pipe();
462        let mut src = EventSource::new(rx)
463            .unwrap()
464            .with_esc_timeout(Duration::from_millis(20))
465            .with_paste_idle_timeout(Some(Duration::from_secs(5)));
466        write_bytes(&tx, b"\x1b[200~body");
467        let _ = src.poll(Some(Duration::from_millis(50))).unwrap();
468        while src.try_read().is_some() {}
469
470        // Send only the beginning of the terminator: a partial ESC
471        // sequence at the head of pending. The esc_timeout (20 ms)
472        // must NOT fire — only the paste timeout (5 s) governs here.
473        write_bytes(&tx, b"\x1b[20");
474        let _ = src.poll(Some(Duration::from_millis(80))).unwrap();
475        let mut saw_esc = false;
476        while let Some(ev) = src.try_read() {
477            if matches!(ev, Event::KeyPress(ref k) if k.code == KeyCode::Escape) {
478                saw_esc = true;
479            }
480        }
481        assert!(!saw_esc, "esc deadline must not fire during paste");
482
483        // Complete the terminator: paste ends cleanly.
484        write_bytes(&tx, b"1~");
485        assert!(src.poll(Some(Duration::from_secs(1))).unwrap());
486        let mut saw_end = false;
487        while let Some(ev) = src.try_read() {
488            if matches!(ev, Event::PasteEnd) {
489                saw_end = true;
490            }
491        }
492        assert!(saw_end);
493    }
494
495    #[test]
496    fn esc_deadline_tightens_long_caller_timeout() {
497        let (rx, tx) = make_pipe();
498        let mut src = EventSource::new(rx)
499            .unwrap()
500            .with_esc_timeout(Duration::from_millis(20));
501        write_byte(&tx, 0x1b);
502        let _ = src.poll(Some(Duration::from_secs(60))).unwrap();
503        let start = Instant::now();
504        assert!(src.poll(Some(Duration::from_secs(60))).unwrap());
505        let ev = src.read().unwrap();
506        let elapsed = start.elapsed();
507        assert!(matches!(ev, Event::KeyPress(k) if k.code == KeyCode::Escape));
508        assert!(
509            elapsed < Duration::from_millis(500),
510            "elapsed = {:?}",
511            elapsed
512        );
513    }
514
515    #[test]
516    fn paste_end_after_chunk_is_delivered_without_extra_input() {
517        // Regression: when a paste body and its closing terminator arrive
518        // in the same read, the decoder returns the chunk first and queues
519        // PasteEnd on its internal pending list. The source must drain that
520        // queued event in the same drain pass — otherwise PasteEnd would
521        // stall until the next byte showed up.
522        let (rx, tx) = make_pipe();
523        let mut src = new_reader(rx);
524        write_bytes(&tx, b"\x1b[200~hello\x1b[201~");
525        assert!(src.poll(Some(Duration::from_secs(1))).unwrap());
526        assert!(matches!(src.read().unwrap(), Event::PasteStart));
527        assert!(matches!(src.read().unwrap(), Event::PasteChunk(ref b) if b == b"hello"));
528        assert!(matches!(src.read().unwrap(), Event::PasteEnd));
529    }
530
531    #[test]
532    fn handle_resize_false_suppresses_sigwinch_resize_event() {
533        // With resize handling disabled (the host has enabled in-band
534        // reports), a SIGWINCH must drain its wake pipe but surface no
535        // Event::Resize — the decoder delivers resizes in-band instead.
536        let stderr_fd = 2;
537        let ws: libc::winsize = unsafe { std::mem::zeroed() };
538        let probe = unsafe { libc::ioctl(stderr_fd, libc::TIOCGWINSZ, &ws as *const _) };
539        if probe < 0 {
540            return;
541        }
542        let stderr_dup = unsafe { libc::dup(stderr_fd) };
543        assert!(stderr_dup >= 0);
544        let stderr_file = unsafe { File::from_raw_fd(stderr_dup) };
545        let mut src = new_reader(stderr_file);
546        assert!(src.handle_resize());
547        src.set_handle_resize(false);
548        assert!(!src.handle_resize());
549        src.last_size = None;
550        unsafe { libc::raise(libc::SIGWINCH) };
551        // No event is produced; the poll runs to its (short) timeout.
552        assert!(!src.poll(Some(Duration::from_millis(50))).unwrap());
553        assert!(src.try_read().is_none());
554    }
555
556    #[test]
557    fn sigwinch_surfaces_resize_event() {
558        // SIGWINCH requires a real tty to query TIOCGWINSZ on. Dup
559        // stderr — under cargo test it is typically a tty — and use
560        // that fd as the input source. If stderr isn't a tty, skip.
561        let stderr_fd = 2;
562        let ws: libc::winsize = unsafe { std::mem::zeroed() };
563        let probe = unsafe { libc::ioctl(stderr_fd, libc::TIOCGWINSZ, &ws as *const _) };
564        if probe < 0 {
565            return;
566        }
567        let stderr_dup = unsafe { libc::dup(stderr_fd) };
568        assert!(stderr_dup >= 0);
569        let stderr_file = unsafe { File::from_raw_fd(stderr_dup) };
570        let mut src = new_reader(stderr_file);
571        // Force a mismatched cached size so the SIGWINCH path surfaces
572        // the dedupe-suppressed event.
573        src.last_size = None;
574        unsafe { libc::raise(libc::SIGWINCH) };
575        assert!(src.poll(Some(Duration::from_secs(1))).unwrap());
576        let ev = src.read().unwrap();
577        assert!(matches!(ev, Event::Resize(_)));
578    }
579}