use crate::framework::control_queue::ControlQueue;
#[cfg(feature = "async")]
use crate::poll::PollTokio;
use crate::poll::{PollQuit, PollRendered, PollTasks, PollTimers};
use crate::run_config::{RunConfig, TermInit};
use crate::{Control, SalsaAppContext, SalsaContext};
use poll_queue::PollQueue;
use rat_event::util::set_have_keyboard_enhancement;
use ratatui_core::buffer::Buffer;
use ratatui_core::layout::Rect;
use ratatui_core::terminal::Frame;
use ratatui_crossterm::crossterm::ExecutableCommand;
use ratatui_crossterm::crossterm::cursor::{DisableBlinking, EnableBlinking, SetCursorStyle};
use ratatui_crossterm::crossterm::event::{
DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture,
};
#[cfg(not(windows))]
use ratatui_crossterm::crossterm::event::{
PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags,
};
#[cfg(not(windows))]
use ratatui_crossterm::crossterm::terminal::supports_keyboard_enhancement;
use ratatui_crossterm::crossterm::terminal::{
EnterAlternateScreen, LeaveAlternateScreen, SetTitle, disable_raw_mode, enable_raw_mode,
};
use std::any::TypeId;
use std::cmp::min;
use std::io::stdout;
use std::panic::{AssertUnwindSafe, catch_unwind, resume_unwind};
use std::time::{Duration, SystemTime};
use std::{io, thread};
pub(crate) mod control_queue;
mod poll_queue;
const SLEEP: u64 = 250_000; const BACKOFF: u64 = 10_000; const FAST_SLEEP: u64 = 100;
fn _run_tui<Global, State, Event, Error>(
init: fn(
state: &mut State, //
ctx: &mut Global,
) -> Result<(), Error>,
render: fn(
area: Rect, //
buf: &mut Buffer,
state: &mut State,
ctx: &mut Global,
) -> Result<(), Error>,
event: fn(
event: &Event, //
state: &mut State,
ctx: &mut Global,
) -> Result<Control<Event>, Error>,
error: fn(
error: Error, //
state: &mut State,
ctx: &mut Global,
) -> Result<Control<Event>, Error>,
global: &mut Global,
state: &mut State,
cfg: RunConfig<Event, Error>,
) -> Result<(), Error>
where
Global: SalsaContext<Event, Error>,
Event: 'static,
Error: 'static + From<io::Error>,
{
let term = cfg.term;
let mut poll = cfg.poll;
let timers = poll.iter().find_map(|v| {
v.as_any()
.downcast_ref::<PollTimers>()
.map(|t| t.get_timers())
});
let tasks = poll.iter().find_map(|v| {
v.as_any()
.downcast_ref::<PollTasks<Event, Error>>()
.map(|t| t.get_tasks())
});
let rendered_event = poll
.iter()
.position(|v| v.as_ref().type_id() == TypeId::of::<PollRendered>());
let quit = poll
.iter()
.position(|v| v.as_ref().type_id() == TypeId::of::<PollQuit>());
#[cfg(feature = "async")]
let tokio = poll.iter().find_map(|v| {
v.as_any()
.downcast_ref::<PollTokio<Event, Error>>()
.map(|t| t.get_tasks())
});
global.set_salsa_ctx(SalsaAppContext {
focus: Default::default(),
count: Default::default(),
cursor: Default::default(),
term: Some(term.clone()),
window_title: Default::default(),
clear_terminal: Default::default(),
insert_before: Default::default(),
last_render: Default::default(),
last_event: Default::default(),
timers,
tasks,
#[cfg(feature = "async")]
tokio,
queue: ControlQueue::default(),
});
let poll_queue = PollQueue::default();
let mut poll_sleep = Duration::from_micros(SLEEP);
let mut was_changed = false;
init(state, global)?;
{
let ib = global.salsa_ctx().insert_before.take();
if ib.height > 0 {
term.borrow_mut().insert_before(ib.height, ib.draw_fn)?;
}
if let Some(title) = global.salsa_ctx().window_title.replace(None) {
stdout().execute(SetTitle(title))?;
}
let mut r = Ok(());
term.borrow_mut().draw(&mut |frame: &mut Frame| -> () {
let frame_area = frame.area();
let ttt = SystemTime::now();
r = render(frame_area, frame.buffer_mut(), state, global);
global
.salsa_ctx()
.last_render
.set(ttt.elapsed().unwrap_or_default());
if let Some((cursor_x, cursor_y)) = global.salsa_ctx().cursor.get() {
frame.set_cursor_position((cursor_x, cursor_y));
}
global.salsa_ctx().count.set(frame.count());
global.salsa_ctx().cursor.set(None);
})?;
r?;
if let Some(idx) = rendered_event {
global.salsa_ctx().queue.push(poll[idx].read());
}
}
'ui: loop {
if let Some(tasks) = &global.salsa_ctx().tasks {
if !tasks.check_liveness() {
dbg!("worker panicked");
break 'ui;
}
}
if global.salsa_ctx().queue.is_empty() {
if poll_queue.is_empty() {
for (n, p) in poll.iter_mut().enumerate() {
match p.poll() {
Ok(true) => {
poll_queue.push(n);
}
Ok(false) => {}
Err(e) => {
global.salsa_ctx().queue.push(Err(e));
}
}
}
}
if poll_queue.is_empty() {
let mut t = poll_sleep;
for p in poll.iter_mut() {
if let Some(timer_sleep) = p.sleep_time() {
t = min(timer_sleep, t);
}
}
thread::sleep(t);
if poll_sleep < Duration::from_micros(SLEEP) {
poll_sleep += Duration::from_micros(BACKOFF);
}
} else {
poll_sleep = Duration::from_micros(FAST_SLEEP);
}
}
if global.salsa_ctx().queue.is_empty() {
if let Some(h) = poll_queue.take() {
global.salsa_ctx().queue.push(poll[h].read());
}
}
if let Some(ctrl) = global.salsa_ctx().queue.take() {
if matches!(ctrl, Ok(Control::Changed)) {
if was_changed {
continue;
}
was_changed = true;
} else {
was_changed = false;
}
match ctrl {
Err(e) => {
let r = error(e, state, global);
global.salsa_ctx().queue.push(r);
}
Ok(Control::Continue) => {}
Ok(Control::Unchanged) => {}
Ok(Control::Changed) => {
if global.salsa_ctx().clear_terminal.get() {
global.salsa_ctx().clear_terminal.set(false);
if let Err(e) = term.borrow_mut().clear() {
global.salsa_ctx().queue.push(Err(e.into()));
}
}
let ib = global.salsa_ctx().insert_before.take();
if ib.height > 0 {
term.borrow_mut().insert_before(ib.height, ib.draw_fn)?;
}
if let Some(title) = global.salsa_ctx().window_title.replace(None) {
stdout().execute(SetTitle(title))?;
}
let mut r = Ok(());
term.borrow_mut().draw(&mut |frame: &mut Frame| -> () {
let frame_area = frame.area();
let ttt = SystemTime::now();
r = render(frame_area, frame.buffer_mut(), state, global);
global
.salsa_ctx()
.last_render
.set(ttt.elapsed().unwrap_or_default());
if let Some((cursor_x, cursor_y)) = global.salsa_ctx().cursor.get() {
frame.set_cursor_position((cursor_x, cursor_y));
}
global.salsa_ctx().count.set(frame.count());
global.salsa_ctx().cursor.set(None);
})?;
match r {
Ok(_) => {
if let Some(h) = rendered_event {
global.salsa_ctx().queue.push(poll[h].read());
}
}
Err(e) => global.salsa_ctx().queue.push(Err(e)),
}
}
#[cfg(feature = "dialog")]
Ok(Control::Close(a)) => {
global.salsa_ctx().queue.push(Ok(Control::Event(a)));
global.salsa_ctx().queue.push(Ok(Control::Changed));
}
Ok(Control::Event(a)) => {
let ttt = SystemTime::now();
let r = event(&a, state, global);
global
.salsa_ctx()
.last_event
.set(ttt.elapsed().unwrap_or_default());
global.salsa_ctx().queue.push(r);
}
Ok(Control::Quit) => {
if let Some(quit) = quit {
match poll[quit].read() {
Ok(Control::Event(a)) => {
match event(&a, state, global) {
Ok(Control::Quit) => { }
v => {
global.salsa_ctx().queue.push(v);
continue;
}
}
}
Err(_) => unreachable!(),
Ok(_) => unreachable!(),
}
}
break 'ui;
}
}
}
}
if cfg.term_init.clear_area {
term.borrow_mut().clear()?;
}
Ok(())
}
pub fn run_tui<Global, State, Event, Error>(
init: fn(
state: &mut State, //
ctx: &mut Global,
) -> Result<(), Error>,
render: fn(
area: Rect, //
buf: &mut Buffer,
state: &mut State,
ctx: &mut Global,
) -> Result<(), Error>,
event: fn(
event: &Event, //
state: &mut State,
ctx: &mut Global,
) -> Result<Control<Event>, Error>,
error: fn(
error: Error, //
state: &mut State,
ctx: &mut Global,
) -> Result<Control<Event>, Error>,
global: &mut Global,
state: &mut State,
cfg: RunConfig<Event, Error>,
) -> Result<(), Error>
where
Global: SalsaContext<Event, Error>,
Event: 'static,
Error: 'static + From<io::Error>,
{
let t = cfg.term_init;
if !t.manual {
init_terminal(t)?;
}
let r = match catch_unwind(AssertUnwindSafe(|| {
_run_tui(init, render, event, error, global, state, cfg)
})) {
Ok(v) => v,
Err(e) => {
if !t.manual {
_ = shutdown_terminal(t);
}
resume_unwind(e);
}
};
if !t.manual {
shutdown_terminal(t)?;
}
r
}
fn init_terminal(cfg: TermInit) -> io::Result<()> {
if cfg.alternate_screen {
stdout().execute(EnterAlternateScreen)?;
}
if cfg.mouse_capture {
stdout().execute(EnableMouseCapture)?;
}
if cfg.bracketed_paste {
stdout().execute(EnableBracketedPaste)?;
}
if cfg.cursor_blinking {
stdout().execute(EnableBlinking)?;
}
stdout().execute(cfg.cursor)?;
#[cfg(not(windows))]
{
stdout().execute(PushKeyboardEnhancementFlags(cfg.keyboard_enhancements))?;
let enhanced = supports_keyboard_enhancement().unwrap_or_default();
set_have_keyboard_enhancement(enhanced);
}
#[cfg(windows)]
{
set_have_keyboard_enhancement(true);
}
if cfg.raw_mode {
enable_raw_mode()?;
}
Ok(())
}
fn shutdown_terminal(cfg: TermInit) -> io::Result<()> {
if cfg.raw_mode {
disable_raw_mode()?;
}
#[cfg(not(windows))]
stdout().execute(PopKeyboardEnhancementFlags)?;
stdout().execute(SetCursorStyle::DefaultUserShape)?;
if cfg.cursor_blinking {
stdout().execute(DisableBlinking)?;
}
if cfg.bracketed_paste {
stdout().execute(DisableBracketedPaste)?;
}
if cfg.mouse_capture {
stdout().execute(DisableMouseCapture)?;
}
if cfg.alternate_screen {
stdout().execute(LeaveAlternateScreen)?;
}
Ok(())
}