use std::{fmt::Debug, sync::Arc};
use async_trait::async_trait;
use get_size::GetSize;
use r3bl_redux::*;
use r3bl_rs_utils_core::*;
use r3bl_rs_utils_macro::*;
use tokio::sync::{mpsc, RwLock};
use crate::*;
pub struct TerminalWindow;
#[derive(Debug)]
pub enum ParallelExecutionPolicy {
Parallel,
Serial,
}
pub const PARALLEL_EXECUTION_POLICY: ParallelExecutionPolicy =
ParallelExecutionPolicy::Serial;
impl TerminalWindow {
pub async fn main_event_loop<S, A>(
shared_app: SharedApp<S, A>,
store: Store<S, A>,
exit_keys: Vec<InputEvent>,
) -> CommonResult<()>
where
S: Debug + Default + Clone + PartialEq + Sync + Send + 'static,
A: Debug + Default + Clone + Sync + Send + 'static,
{
let _global_data = GlobalData::try_to_create_instance()?;
let shared_global_data: SharedGlobalData = Arc::new(RwLock::new(_global_data));
RawMode::start(&shared_global_data).await;
let shared_store: SharedStore<S, A> = Arc::new(RwLock::new(store));
let _subscriber =
AppManager::new_box(&shared_app, &shared_store, &shared_global_data);
shared_store.write().await.add_subscriber(_subscriber).await;
let mut async_event_stream = AsyncEventStream::default();
AppManager::render_app(&shared_store, &shared_app, &shared_global_data, None)
.await?;
shared_global_data
.read()
.await
.dump_to_log("main_event_loop -> Startup 🚀");
let (exit_channel_sender, mut exit_channel_reciever) = mpsc::channel::<bool>(1);
loop {
tokio::select! {
result = exit_channel_reciever.recv() => {
if result.is_some() {
RawMode::end(&shared_global_data).await;
break;
}
}
maybe_input_event = async_event_stream.try_to_get_input_event() => {
if let Some(input_event) = maybe_input_event {
telemetry_global_static::set_start_ts();
call_if_true!(DEBUG_TUI_MOD, {
let msg = format!("main_event_loop -> Tick: ⏰ {input_event}");
log_info(msg);
});
Self::handle_resize_event(&input_event, &shared_global_data, &shared_store, &shared_app).await;
match PARALLEL_EXECUTION_POLICY {
ParallelExecutionPolicy::Parallel => {
let shared_global_data = shared_global_data.clone();
let shared_store = shared_store.clone();
let shared_app = shared_app.clone();
let input_event = input_event.clone();
let exit_keys = exit_keys.clone();
let exit_channel_sender = exit_channel_sender.clone();
tokio::spawn(async move {
Self::actually_process_input_event(
shared_global_data,
shared_store,
shared_app,
input_event,
exit_keys,
exit_channel_sender,
).await;
});
}
ParallelExecutionPolicy::Serial => {
Self::actually_process_input_event(
shared_global_data.clone(),
shared_store.clone(),
shared_app.clone(),
input_event.clone(),
exit_keys.clone(),
exit_channel_sender.clone(),
).await;
}
}
}
}
}
}
Ok(())
}
async fn actually_process_input_event<S, A>(
shared_global_data: SharedGlobalData,
shared_store: SharedStore<S, A>,
shared_app: SharedApp<S, A>,
input_event: InputEvent,
exit_keys: Vec<InputEvent>,
exit_channel_sender: mpsc::Sender<bool>,
) where
S: Debug + Default + Clone + PartialEq + Sync + Send + 'static,
A: Debug + Default + Clone + Sync + Send + 'static,
{
let result = AppManager::route_input_to_app(
shared_global_data.clone(),
shared_store.clone(),
shared_app.clone(),
input_event.clone(),
)
.await;
call_if_true!(DEBUG_TUI_MOD, {
let msg = format!(
"main_event_loop -> 🚥 SPAWN propagation_result_from_app: {result:?}"
);
log_info(msg);
});
if let Ok(event_propagation) = result {
match event_propagation {
EventPropagation::Propagate => {
let check_if_exit_keys_pressed = DefaultInputEventHandler::no_consume(
input_event.clone(),
&exit_keys,
);
if let Continuation::Exit = check_if_exit_keys_pressed.await {
let _ = exit_channel_sender.send(true).await;
};
}
EventPropagation::ConsumedRender => {
let _ = AppManager::render_app(
&shared_store,
&shared_app,
&shared_global_data,
None,
)
.await;
}
EventPropagation::Consumed => {}
EventPropagation::ExitMainEventLoop => {
let _ = exit_channel_sender.send(true).await;
}
}
}
}
pub async fn handle_resize_event<S, A>(
input_event: &InputEvent,
shared_global_data: &SharedGlobalData,
shared_store: &SharedStore<S, A>,
shared_app: &SharedApp<S, A>,
) where
S: Debug + Default + Clone + PartialEq + Sync + Send + 'static,
A: Debug + Default + Clone + Sync + Send + 'static,
{
if let InputEvent::Resize(new_size) = input_event {
shared_global_data.write().await.set_size(*new_size);
shared_global_data
.write()
.await
.maybe_saved_offscreen_buffer = None;
let _ = AppManager::render_app(
shared_store,
shared_app,
shared_global_data,
None,
)
.await;
}
}
}
struct AppManager<S, A>
where
S: Debug + Default + Clone + PartialEq + Sync + Send + 'static,
A: Debug + Default + Clone + Sync + Send + 'static,
{
shared_app: SharedApp<S, A>,
shared_store: SharedStore<S, A>,
shared_global_data: SharedGlobalData,
}
#[async_trait]
impl<S, A> AsyncSubscriber<S> for AppManager<S, A>
where
S: Debug + Default + Clone + PartialEq + Sync + Send + 'static,
A: Debug + Default + Clone + Sync + Send,
{
async fn run(&self, my_state: S) {
let result = AppManager::render_app(
&self.shared_store,
&self.shared_app,
&self.shared_global_data,
my_state.into(),
)
.await;
if let Err(e) = result {
call_if_true!(DEBUG_TUI_MOD, {
let msg = format!("MySubscriber::run -> Error: {e}");
log_error(msg);
})
}
}
}
impl<S, A> AppManager<S, A>
where
S: Debug + Default + Clone + PartialEq + Sync + Send + 'static,
A: Debug + Default + Clone + Sync + Send,
{
fn new_box(
shared_app: &SharedApp<S, A>,
shared_store: &SharedStore<S, A>,
shared_global_data: &SharedGlobalData,
) -> Box<Self> {
Box::new(AppManager {
shared_app: shared_app.clone(),
shared_store: shared_store.clone(),
shared_global_data: shared_global_data.clone(),
})
}
pub async fn route_input_to_app(
shared_global_data: SharedGlobalData,
shared_store: SharedStore<S, A>,
shared_app: SharedApp<S, A>,
input_event: InputEvent,
) -> CommonResult<EventPropagation> {
throws_with_return!({
let state = shared_store.read().await.get_state();
let window_size = shared_global_data.read().await.get_size();
let global_scope_args = GlobalScopeArgs {
shared_global_data: &shared_global_data,
shared_store: &shared_store,
state: &state,
window_size: &window_size,
};
shared_app
.write()
.await
.app_handle_event(global_scope_args, &input_event)
.await?
});
}
pub async fn render_app(
shared_store: &SharedStore<S, A>,
shared_app: &SharedApp<S, A>,
shared_global_data: &SharedGlobalData,
maybe_state: Option<S>,
) -> CommonResult<()> {
throws!({
let window_size = shared_global_data.read().await.get_size();
let state: S = if let Some(state) = maybe_state {
state
} else {
shared_store.read().await.get_state()
};
let global_scope_args = GlobalScopeArgs {
state: &state,
shared_store,
shared_global_data,
window_size: &window_size,
};
let render_result: CommonResult<RenderPipeline> = if window_size
.is_too_small_to_display(MinSize::Col as u8, MinSize::Row as u8)
{
shared_global_data
.write()
.await
.maybe_saved_offscreen_buffer = None;
Ok(render_window_size_too_small(window_size))
} else {
shared_app.write().await.app_render(global_scope_args).await
};
match render_result {
Err(error) => {
RenderOp::default().flush();
telemetry_global_static::set_end_ts();
call_if_true!(DEBUG_TUI_MOD, {
let msg = format!("MySubscriber::render() error ❌: {error}");
log_error(msg);
});
}
Ok(render_pipeline) => {
render_pipeline
.paint(FlushKind::ClearBeforeFlush, shared_global_data)
.await;
telemetry_global_static::set_end_ts();
call_if_true!(DEBUG_TUI_MOD, {
{
let msg_1 = format!("🎨 MySubscriber::paint() ok ✅: \n window_size: {window_size:?}\n state: {state:?}");
let msg_2 = {
format!(
"🌍⏳ SPEED: {:?}",
telemetry_global_static::get_avg_response_time_micros(
),
)
};
if let Some(ref offscreen_buffer) = &shared_global_data
.read()
.await
.maybe_saved_offscreen_buffer
{
let msg_3 = format!(
"offscreen_buffer: {0:.2}kb",
offscreen_buffer.get_size() as f32 / 1000_f32
);
log_info(format!("{msg_1}\n{msg_2}, {msg_3}"));
} else {
log_info(format!("{msg_1}\n{msg_2}"));
}
}
});
}
}
});
}
}
fn render_window_size_too_small(window_size: Size) -> RenderPipeline {
let display_msg = UnicodeString::from(format!(
"Window size is too small. Minimum size is {} cols x {} rows",
MinSize::Col as u8,
MinSize::Row as u8
));
let trunc_display_msg =
UnicodeString::from(display_msg.truncate_to_fit_size(window_size));
let trunc_display_msg_len = ch!(trunc_display_msg.len());
let row_pos = window_size.row_count / 2;
let col_pos = (window_size.col_count - trunc_display_msg_len) / 2;
let mut pipeline = render_pipeline!();
let style_bold = style!(attrib: [bold]);
render_pipeline! {
@push_into pipeline
at ZOrder::Normal
=>
RenderOp::ResetColor,
RenderOp::MoveCursorPositionAbs(position! {col_index: col_pos, row_index: row_pos})
}
render_pipeline! {
@push_styled_texts_into pipeline
at ZOrder::Normal
=>
ColorWheel::new(vec![
ColorWheelConfig::RgbRandom(ColorWheelSpeed::Fast),
ColorWheelConfig::Ansi256(Ansi256GradientIndex::DarkRedToDarkMagenta, ColorWheelSpeed::Medium),
])
.colorize_into_styled_texts(
&trunc_display_msg,
GradientGenerationPolicy::RegenerateGradientAndIndexBasedOnTextLength,
TextColorizationPolicy::ColorEachCharacter(Some(style_bold)),
)
}
pipeline
}