use crate::app_event::{AppEvent, AppEventReceiver, AppEventSender};
use crossterm::event::EventStream;
use futures::StreamExt;
use std::io::{self, stdout, Stdout};
use std::time::Duration;
use tokio::time::interval;
use crossterm::{
event::{
DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture,
KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags,
},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{backend::CrosstermBackend, Terminal};
pub type TuiTerminal = Terminal<CrosstermBackend<Stdout>>;
const TICK_RATE_MS: u64 = 16;
const MIN_RENDER_INTERVAL_MS: u64 = 16;
pub struct TuiRunnerConfig {
pub mouse_capture: bool,
pub keyboard_enhancement: bool,
pub alternate_screen: bool,
}
impl Default for TuiRunnerConfig {
fn default() -> Self {
Self {
mouse_capture: true,
keyboard_enhancement: true,
alternate_screen: false, }
}
}
pub fn init_terminal(config: &TuiRunnerConfig) -> io::Result<TuiTerminal> {
enable_raw_mode()?;
if config.alternate_screen {
execute!(stdout(), EnterAlternateScreen)?;
}
if config.keyboard_enhancement {
let _ = execute!(
stdout(),
PushKeyboardEnhancementFlags(
KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
| KeyboardEnhancementFlags::REPORT_EVENT_TYPES
)
);
}
execute!(stdout(), EnableBracketedPaste)?;
if config.mouse_capture {
execute!(stdout(), EnableMouseCapture)?;
}
let backend = CrosstermBackend::new(stdout());
Terminal::new(backend)
}
pub fn restore_terminal(config: &TuiRunnerConfig) -> io::Result<()> {
if config.keyboard_enhancement {
let _ = execute!(stdout(), PopKeyboardEnhancementFlags);
}
if config.mouse_capture {
let _ = execute!(stdout(), DisableMouseCapture);
}
execute!(stdout(), DisableBracketedPaste)?;
if config.alternate_screen {
execute!(stdout(), LeaveAlternateScreen)?;
}
disable_raw_mode()?;
Ok(())
}
pub async fn run_event_loop(
app_tx: AppEventSender,
mut app_rx: AppEventReceiver,
mut on_event: impl FnMut(AppEvent) -> bool, ) {
let mut event_stream = EventStream::new();
let mut tick_interval = interval(Duration::from_millis(TICK_RATE_MS));
loop {
tokio::select! {
maybe_event = event_stream.next() => {
match maybe_event {
Some(Ok(event)) => {
if !on_event(AppEvent::Terminal(event)) {
break;
}
}
Some(Err(e)) => {
let _ = app_tx.send(AppEvent::Error(e.to_string()));
}
None => break, }
}
Some(event) = app_rx.recv() => {
if !on_event(event) {
break;
}
}
_ = tick_interval.tick() => {
if !on_event(AppEvent::Tick) {
break;
}
}
}
}
}
pub struct FrameRateLimiter {
last_render: std::time::Instant,
min_interval: Duration,
}
impl Default for FrameRateLimiter {
fn default() -> Self {
Self {
last_render: std::time::Instant::now(),
min_interval: Duration::from_millis(MIN_RENDER_INTERVAL_MS),
}
}
}
impl FrameRateLimiter {
pub fn should_render(&mut self) -> bool {
let now = std::time::Instant::now();
if now.duration_since(self.last_render) >= self.min_interval {
self.last_render = now;
true
} else {
false
}
}
pub fn force_render(&mut self) {
self.last_render = std::time::Instant::now();
}
}