use std::io::{self, Stdout};
use crate::error;
use crossterm::ExecutableCommand;
use crossterm::event::{DisableMouseCapture, EnableMouseCapture};
use crossterm::terminal::{
EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
};
use ratatui::backend::CrosstermBackend;
use super::Runtime;
use super::config::RuntimeConfig;
use crate::app::model::App;
pub fn restore_terminal() -> crate::error::Result<()> {
disable_raw_mode()?;
io::stdout().execute(LeaveAlternateScreen)?;
io::stdout().execute(DisableMouseCapture)?;
crossterm::execute!(io::stdout(), crossterm::cursor::Show)?;
Ok(())
}
use crate::overlay::OverlayAction;
impl<A: App> Runtime<A, CrosstermBackend<Stdout>> {
pub async fn run_terminal(mut self) -> error::Result<A::State> {
use futures_util::StreamExt;
#[cfg(feature = "tracing")]
tracing::info!("starting terminal runtime loop");
let mut tick_interval = tokio::time::interval(self.config.tick_rate);
let mut render_interval = tokio::time::interval(self.config.frame_rate);
let mut event_stream = crossterm::event::EventStream::new();
self.render()?;
let result = loop {
tokio::select! {
maybe_event = event_stream.next() => {
match maybe_event {
Some(Ok(event)) => {
if let Some(envision_event) = crate::input::convert::from_crossterm_event(event) {
#[cfg(feature = "tracing")]
tracing::debug!(event = ?envision_event, "terminal received event");
match self.core.overlay_stack.handle_event(&envision_event) {
OverlayAction::Consumed => {}
OverlayAction::KeepAndMessage(msg) => self.dispatch(msg),
OverlayAction::Dismiss => {
self.core.overlay_stack.pop();
}
OverlayAction::DismissWithMessage(msg) => {
self.core.overlay_stack.pop();
self.dispatch(msg);
}
OverlayAction::Propagate => {
if let Some(msg) =
A::handle_event_with_state(&self.core.state, &envision_event)
{
self.dispatch(msg);
}
}
}
}
}
Some(Err(e)) => {
break Err(e.into());
}
None => {
break Ok(());
}
}
}
Some(msg) = self.message_rx.recv() => {
#[cfg(feature = "tracing")]
tracing::debug!("terminal received async message");
self.dispatch(msg);
}
_ = tick_interval.tick() => {
self.process_commands();
let mut messages_processed = 0;
while self.process_event() && messages_processed < self.core.max_messages_per_tick {
messages_processed += 1;
}
if let Some(msg) = A::on_tick(&self.core.state) {
self.dispatch(msg);
}
if A::should_quit(&self.core.state) {
self.core.should_quit = true;
}
}
_ = render_interval.tick() => {
if let Err(e) = self.render() {
break Err(e);
}
}
_ = self.cancel_token.cancelled() => {
#[cfg(feature = "tracing")]
tracing::info!("terminal received cancellation");
self.core.should_quit = true;
}
}
if self.core.should_quit {
break Ok(());
}
};
let cleanup_result = self.cleanup_terminal();
A::on_exit(&self.core.state);
result.and(cleanup_result)?;
Ok(self.core.state)
}
pub fn run_terminal_blocking(self) -> error::Result<A::State> {
let rt = tokio::runtime::Runtime::new().map_err(io::Error::other)?;
rt.block_on(self.run_terminal())
}
pub(super) fn setup_terminal(
config: &RuntimeConfig,
) -> error::Result<CrosstermBackend<Stdout>> {
enable_raw_mode()?;
let mut stdout = io::stdout();
stdout.execute(EnterAlternateScreen)?;
stdout.execute(EnableMouseCapture)?;
if let Some(ref hook) = config.on_setup {
hook()?;
}
Ok(CrosstermBackend::new(stdout))
}
fn cleanup_terminal(&mut self) -> error::Result<()> {
if let Some(ref hook) = self.config.on_teardown {
hook()?;
}
disable_raw_mode()?;
self.core
.terminal
.backend_mut()
.execute(LeaveAlternateScreen)?;
self.core
.terminal
.backend_mut()
.execute(DisableMouseCapture)?;
self.core.terminal.show_cursor()?;
Ok(())
}
}