use std::io::{self, stdout};
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use crossterm::event;
use crate::app::context::SurfaceMode;
use super::native_terminal::surface_terminal_policy;
use super::terminal_transition::{
CrosstermTransitionExecutor, execute_plan, execute_plan_with_rollback, resume_plan,
suspend_plan,
};
static STDIN_READER_PAUSED: AtomicBool = AtomicBool::new(false);
static FULL_REPAINT_AFTER_HANDOFF: AtomicBool = AtomicBool::new(false);
const READER_PAUSE_SETTLE: Duration = Duration::from_millis(125);
pub(crate) fn stdin_reader_is_paused() -> bool {
STDIN_READER_PAUSED.load(Ordering::SeqCst)
}
pub(crate) fn take_handoff_full_repaint_request() -> bool {
FULL_REPAINT_AFTER_HANDOFF.swap(false, Ordering::SeqCst)
}
pub(crate) fn reset_handoff_state_for_terminal_restore() {
STDIN_READER_PAUSED.store(false, Ordering::SeqCst);
FULL_REPAINT_AFTER_HANDOFF.store(false, Ordering::SeqCst);
}
pub(crate) struct StdinReaderPauseGuard {
flush_on_drop: bool,
}
impl Drop for StdinReaderPauseGuard {
fn drop(&mut self) {
if self.flush_on_drop
&& let Err(err) = discard_pending_terminal_input()
{
crate::debug::internal_log!(
"[tui-lipan] terminal_handoff: discard pending input failed (non-fatal): {}",
err
);
}
STDIN_READER_PAUSED.store(false, Ordering::SeqCst);
}
}
pub(crate) fn pause_stdin_reader_for_terminal_query_with(
wait_for_reader: bool,
flush_on_drop: bool,
) -> StdinReaderPauseGuard {
STDIN_READER_PAUSED.store(true, Ordering::SeqCst);
if wait_for_reader {
std::thread::sleep(READER_PAUSE_SETTLE);
}
StdinReaderPauseGuard { flush_on_drop }
}
pub(crate) fn drain_terminal_query_responses_preserving_input() -> io::Result<Vec<event::Event>> {
let mut preserved = Vec::new();
collect_pending_terminal_events(8192, &mut preserved)?;
#[cfg(unix)]
{
flush_stdin_input_queue_unix();
}
std::thread::sleep(Duration::from_millis(1));
collect_pending_terminal_events(4096, &mut preserved)?;
Ok(preserved)
}
fn collect_pending_terminal_events(
max_events: usize,
preserved: &mut Vec<event::Event>,
) -> io::Result<()> {
for _ in 0..max_events {
if !event::poll(Duration::ZERO)? {
break;
}
let ev = event::read()?;
if is_preservable_input(&ev) {
preserved.push(ev);
}
}
Ok(())
}
fn is_preservable_input(ev: &event::Event) -> bool {
!matches!(ev, event::Event::Key(_))
}
fn discard_pending_terminal_input() -> io::Result<()> {
drain_crossterm_events(8192)?;
#[cfg(unix)]
{
flush_stdin_input_queue_unix();
}
std::thread::sleep(Duration::from_millis(1));
drain_crossterm_events(4096)?;
Ok(())
}
fn drain_crossterm_events(max_events: usize) -> io::Result<()> {
for _ in 0..max_events {
if !event::poll(Duration::ZERO)? {
break;
}
let _ = event::read()?;
}
Ok(())
}
#[cfg(unix)]
fn flush_stdin_input_queue_unix() {
use std::os::unix::io::AsRawFd;
let fd = std::io::stdin().as_raw_fd();
#[allow(unsafe_code)]
let rc = unsafe { libc::tcflush(fd, libc::TCIFLUSH) };
if rc != 0 {
let err = io::Error::last_os_error();
if err.raw_os_error() != Some(libc::EINVAL) && err.raw_os_error() != Some(libc::ENOTTY) {
crate::debug::internal_log!(
"[tui-lipan] terminal_handoff: tcflush stdin failed (non-fatal): {}",
err
);
}
}
}
pub fn suspend_for_external_process(surface_mode: SurfaceMode) -> io::Result<()> {
let policy = surface_terminal_policy(surface_mode);
STDIN_READER_PAUSED.store(true, Ordering::SeqCst);
std::thread::sleep(std::time::Duration::from_millis(10));
let mut out = stdout();
let plan = suspend_plan(policy);
let mut executor = CrosstermTransitionExecutor::new(&mut out);
let result = execute_plan(&mut executor, &plan);
if result.is_err() {
STDIN_READER_PAUSED.store(false, Ordering::SeqCst);
}
result
}
pub fn resume_after_external_process(
surface_mode: SurfaceMode,
mouse_enabled: bool,
) -> io::Result<()> {
let policy = surface_terminal_policy(surface_mode);
let mut out = stdout();
let plan = resume_plan(policy, mouse_enabled);
let mut executor = CrosstermTransitionExecutor::new(&mut out);
if let Err(err) = execute_plan_with_rollback(&mut executor, &plan) {
STDIN_READER_PAUSED.store(false, Ordering::SeqCst);
return Err(err);
}
if let Err(err) = discard_pending_terminal_input() {
crate::debug::internal_log!(
"[tui-lipan] terminal_handoff: discard pending input failed (non-fatal): {}",
err
);
}
STDIN_READER_PAUSED.store(false, Ordering::SeqCst);
FULL_REPAINT_AFTER_HANDOFF.store(true, Ordering::SeqCst);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crossterm::event::KeyModifiers;
use crossterm::event::{Event, KeyCode, KeyEvent, MouseButton, MouseEvent, MouseEventKind};
fn mouse(kind: MouseEventKind) -> Event {
Event::Mouse(MouseEvent {
kind,
column: 0,
row: 0,
modifiers: KeyModifiers::NONE,
})
}
#[test]
fn preserves_genuine_input_drops_key_garbage() {
assert!(is_preservable_input(&mouse(MouseEventKind::ScrollUp)));
assert!(is_preservable_input(&mouse(MouseEventKind::ScrollDown)));
assert!(is_preservable_input(&mouse(MouseEventKind::Down(
MouseButton::Left
))));
assert!(is_preservable_input(&Event::Resize(80, 24)));
assert!(is_preservable_input(&Event::FocusGained));
assert!(is_preservable_input(&Event::FocusLost));
assert!(is_preservable_input(&Event::Paste("x".into())));
assert!(!is_preservable_input(&Event::Key(KeyEvent::new(
KeyCode::Char('a'),
KeyModifiers::NONE
))));
}
}