use std::io::{self, Write};
use std::mem::ManuallyDrop;
use std::panic::{self, PanicHookInfo};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread;
use std::time::Duration;
use crossterm::cursor::{Hide, Show};
use crossterm::event::{self, DisableBracketedPaste, EnableBracketedPaste, Event, EventStream};
use crossterm::execute;
use crossterm::terminal::{
EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
};
use futures::StreamExt;
use ratatui::backend::CrosstermBackend;
use ratatui::{DefaultTerminal, Terminal};
use tokio::sync::mpsc::UnboundedSender;
use tokio::task::JoinHandle;
use tokio_util::sync::CancellationToken;
type Hook = dyn Fn(&PanicHookInfo<'_>) + Send + Sync + 'static;
pub(super) struct TerminalGuard {
previous: Arc<Hook>,
}
impl TerminalGuard {
pub(super) fn enter() -> Self {
let previous: Arc<Hook> = Arc::from(panic::take_hook());
let owner = thread::current().id();
let chained = Arc::clone(&previous);
panic::set_hook(Box::new(move |info| {
if thread::current().id() == owner {
restore();
chained(info);
} else {
tracing::error!("a background task panicked: {info}");
}
}));
Self { previous }
}
}
impl Drop for TerminalGuard {
fn drop(&mut self) {
restore();
if !thread::panicking() {
drop(panic::take_hook());
let previous = Arc::clone(&self.previous);
panic::set_hook(Box::new(move |info| previous(info)));
}
}
}
pub(super) fn init() -> io::Result<ManuallyDrop<DefaultTerminal>> {
RESTORED.store(false, Ordering::SeqCst);
enable_raw_mode()?;
execute!(io::stdout(), EnterAlternateScreen, EnableBracketedPaste)?;
Terminal::new(CrosstermBackend::new(io::stdout())).map(ManuallyDrop::new)
}
static RESTORED: AtomicBool = AtomicBool::new(false);
fn restore() {
if RESTORED.load(Ordering::SeqCst) {
return;
}
execute!(io::stdout(), DisableBracketedPaste).ok();
if let Err(error) = ratatui::try_restore() {
writeln!(io::stderr(), "cannot restore the terminal: {error}").ok();
}
execute!(io::stdout(), Show).ok();
}
pub(super) struct Screen {
reader: Option<InputTask>,
events: UnboundedSender<io::Result<Event>>,
}
impl Screen {
pub(super) fn start(events: UnboundedSender<io::Result<Event>>) -> Self {
Self {
reader: Some(InputTask::start(events.clone())),
events,
}
}
pub(super) async fn suspend(&mut self) -> io::Result<()> {
if let Some(reader) = self.reader.take() {
reader.stop().await;
}
let dropped = drop_keys();
let left = execute!(
io::stdout(),
DisableBracketedPaste,
LeaveAlternateScreen,
Show
);
let normal = disable_raw_mode();
if left.is_ok() && normal.is_ok() {
RESTORED.store(true, Ordering::SeqCst);
}
dropped.and(left).and(normal)
}
pub(super) fn resume(&mut self) -> io::Result<()> {
RESTORED.store(false, Ordering::SeqCst);
enable_raw_mode()?;
execute!(
io::stdout(),
EnterAlternateScreen,
EnableBracketedPaste,
Hide
)?;
self.reader = Some(InputTask::start(self.events.clone()));
Ok(())
}
pub(super) async fn stop(self) {
if let Some(reader) = self.reader {
reader.stop().await;
}
}
}
fn drop_keys() -> io::Result<()> {
while event::poll(Duration::ZERO)? {
event::read()?;
}
Ok(())
}
struct InputTask {
token: CancellationToken,
handle: JoinHandle<()>,
}
impl InputTask {
fn start(events: UnboundedSender<io::Result<Event>>) -> Self {
let token = CancellationToken::new();
let stop = token.clone();
let handle = tokio::spawn(async move {
let mut stream = EventStream::new();
loop {
tokio::select! {
() = stop.cancelled() => break,
event = stream.next() => match event {
Some(Ok(event)) => {
if events.send(Ok(event)).is_err() {
break;
}
},
Some(Err(error)) => {
events.send(Err(error)).ok();
break;
},
None => break,
},
}
}
});
Self { token, handle }
}
async fn stop(self) {
self.token.cancel();
self.handle.await.ok();
}
}