use std::{future::Future, io};
use anyhow::{anyhow, Context};
use crossterm::{
cursor::{MoveTo, Show},
event::Event,
execute,
style::Print,
terminal::{disable_raw_mode, Clear, ClearType, LeaveAlternateScreen},
};
use ratatui::DefaultTerminal;
use super::{keyboard_modes, mouse_capture, terminal_events::TerminalEvents};
pub(super) struct SuspendedRun<T> {
pub(super) operation_result: anyhow::Result<T>,
pub(super) resume_result: anyhow::Result<()>,
}
pub(super) struct TerminalSession {
events: Option<TerminalEvents>,
keyboard: Option<keyboard_modes::Enabled>,
mouse_capture_enabled: bool,
}
impl TerminalSession {
pub(super) fn acquire() -> Self {
Self {
events: Some(TerminalEvents::new()),
keyboard: Some(keyboard_modes::Enabled::acquire()),
mouse_capture_enabled: mouse_capture::enable().is_ok(),
}
}
pub(super) async fn next_event(&mut self) -> std::io::Result<Event> {
self.events
.as_mut()
.expect("terminal events active")
.next()
.await
}
pub(super) async fn run_suspended<T, F, Fut>(
&mut self,
terminal: &mut DefaultTerminal,
handoff_status: &str,
operation: F,
) -> SuspendedRun<T>
where
F: FnOnce() -> Fut,
Fut: Future<Output = anyhow::Result<T>>,
{
self.stop_events();
if let Err(suspend_error) = self.suspend(handoff_status) {
return SuspendedRun {
operation_result: Err(suspend_error.context("suspended operation was not started")),
resume_result: self
.resume(terminal)
.context("failed to recover Rho after terminal suspension failed"),
};
}
let operation_result = operation().await;
let resume_result = self
.resume(terminal)
.context("failed to resume Rho after suspended operation");
SuspendedRun {
operation_result,
resume_result,
}
}
fn stop_events(&mut self) {
self.events = None;
}
fn suspend(&mut self, handoff_status: &str) -> anyhow::Result<()> {
let mut failures = Vec::new();
if self.mouse_capture_enabled {
if let Err(error) = mouse_capture::disable() {
failures.push(format!("disable mouse capture: {error}"));
}
self.mouse_capture_enabled = false;
}
if let Some(keyboard) = self.keyboard.take() {
if let Err(error) = keyboard.try_release() {
failures.push(format!("disable keyboard modes: {error}"));
}
}
if let Err(error) = hand_off_terminal(handoff_status) {
failures.push(format!("restore terminal: {error}"));
}
if failures.is_empty() {
Ok(())
} else {
Err(anyhow!(failures.join("; ")))
}
}
fn resume(&mut self, terminal: &mut DefaultTerminal) -> anyhow::Result<()> {
let resumed = ratatui::try_init().context("initialize terminal")?;
*terminal = resumed;
self.keyboard = Some(keyboard_modes::Enabled::acquire());
self.mouse_capture_enabled = mouse_capture::enable().is_ok();
self.events = Some(TerminalEvents::new());
Ok(())
}
}
fn hand_off_terminal(handoff_status: &str) -> io::Result<()> {
disable_raw_mode()?;
execute!(
io::stdout(),
LeaveAlternateScreen,
Clear(ClearType::All),
MoveTo(0, 0),
Show,
Print(handoff_status),
Print("\r\n"),
)?;
Ok(())
}
impl Drop for TerminalSession {
fn drop(&mut self) {
if let Some(keyboard) = self.keyboard.take() {
keyboard.release();
}
if self.mouse_capture_enabled {
let _ = mouse_capture::disable();
self.mouse_capture_enabled = false;
}
}
}