//! Shared startup terminal-probe primitive: write a query, and (OSC 11 //! only) raw-fd poll/read stdin until a terminator or deadline. //! XTVERSION uses only `write_query`; //! its reply is handled by the event loop's response filter. //! //! Safety invariants (timed-read path): //! - Startup-only: must run before crossterm's `EventStream` exists (both //! compete for stdin). //! - Keystrokes typed inside the read window are consumed and dropped — no //! portable re-injection exists (TIOCSTI is blocked); accepted loss. use std::io::Write; #[cfg(unix)] use std::time::Duration; /// Bounds the reply buffer against terminals that stream without a terminator. #[cfg(unix)] pub(crate) const MAX_PROBE_RESPONSE: usize = 256; /// Hard cap on post-deadline consumption of an in-flight reply. #[cfg(unix)] const LATE_REPLY_GRACE: Duration = Duration::from_millis(100); /// Per-byte quiet window during the grace period. #[cfg(unix)] const LATE_REPLY_QUIET_MS: i32 = 25; /// Write a probe query via the shared stderr lock; `false` if the TUI fd is /// not a TTY or the write fails. pub(crate) fn write_query(query: &[u8]) -> bool { use std::io::IsTerminal; let write_result: std::io::Result<()> = kigi_shared::stderr::with_locked_stderr(|stderr| { // fd 2 is /dev/null-redirected; the TTY check must run on the // dup'd render fd inside the lock, not on std::io::stderr(). if !stderr.is_terminal() { return Err(std::io::Error::other("TUI output is not a TTY")); } stderr.write_all(query)?; stderr.flush() }); write_result.is_ok() } /// Read stdin until `is_terminated`, the size cap, or the deadline. /// /// Returns `Some(buf)` whenever bytes were consumed (even partial, so a /// half-read reply is never left for the EventStream); `None` when nothing /// arrived or stdin errored before any byte. #[cfg(unix)] pub(crate) fn read_tty_reply( timeout: Duration, mut is_terminated: impl FnMut(&[u8], u8) -> bool, ) -> Option> { use std::os::unix::io::AsRawFd; let fd = std::io::stdin().as_raw_fd(); let start = std::time::Instant::now(); let mut buf: Vec = Vec::with_capacity(64); loop { let Some(remaining) = timeout.checked_sub(start.elapsed()) else { return finish_after_deadline(fd, buf, is_terminated); }; let remaining_ms = remaining.as_millis().min(i32::MAX as u128) as i32; match poll_read_byte(fd, remaining_ms) { PollRead::Byte(byte) => { buf.push(byte); if buf.len() >= MAX_PROBE_RESPONSE || is_terminated(&buf, byte) { return Some(buf); } } // Re-entry recomputes the deadline, so EINTR cannot extend it. PollRead::Interrupted => continue, PollRead::Timeout => return finish_after_deadline(fd, buf, is_terminated), PollRead::Error => return if buf.is_empty() { None } else { Some(buf) }, } } } /// Deadline expiry: an in-flight reply (ESC byte seen — replies are /// DCS/CSI/OSC, plain keystrokes aren't) is consumed until quiet so its /// tail can't reach the EventStream as typed garbage; otherwise return /// immediately to avoid eating keystrokes at a silent terminal. #[cfg(unix)] fn finish_after_deadline( fd: i32, mut buf: Vec, mut is_terminated: impl FnMut(&[u8], u8) -> bool, ) -> Option> { if buf.is_empty() { return None; } if !buf.contains(&0x1b) { return Some(buf); } let grace_start = std::time::Instant::now(); while grace_start.elapsed() < LATE_REPLY_GRACE { match poll_read_byte(fd, LATE_REPLY_QUIET_MS) { PollRead::Byte(byte) => { buf.push(byte); if buf.len() >= MAX_PROBE_RESPONSE || is_terminated(&buf, byte) { break; } } PollRead::Interrupted => continue, PollRead::Timeout | PollRead::Error => break, } } Some(buf) } #[cfg(unix)] enum PollRead { Byte(u8), Interrupted, Timeout, Error, } /// One EINTR-retrying poll-then-read step for a single byte. #[cfg(unix)] fn poll_read_byte(fd: i32, timeout_ms: i32) -> PollRead { let mut pfd = libc::pollfd { fd, events: libc::POLLIN, revents: 0, }; // SAFETY: pfd is a valid pollfd struct with a valid fd. let ret = unsafe { libc::poll(&mut pfd, 1, timeout_ms) }; if ret == 0 { return PollRead::Timeout; } if ret < 0 { return if last_errno_is_eintr() { PollRead::Interrupted } else { PollRead::Error }; } loop { let mut byte = [0u8; 1]; // SAFETY: byte is a valid buffer of length 1. let n = unsafe { libc::read(fd, byte.as_mut_ptr().cast(), 1) }; if n == 1 { return PollRead::Byte(byte[0]); } if n < 0 && last_errno_is_eintr() { continue; } return PollRead::Error; } } #[cfg(unix)] fn last_errno_is_eintr() -> bool { std::io::Error::last_os_error().raw_os_error() == Some(libc::EINTR) }