use std::{collections::HashMap, fmt::Debug, sync::Arc};
use async_trait::async_trait;
use int_enum::IntEnum;
use r3bl_redux::*;
use r3bl_rs_utils_core::*;
use r3bl_rs_utils_macro::style;
use tokio::sync::RwLock;
use crate::*;
const SPAWN_PROCESS_INPUT: bool = true;
#[derive(Clone, Debug, Default)]
pub struct TWData {
pub size: Size,
pub cursor_position: Position,
pub maybe_render_pipeline: Option<RenderPipeline>,
pub global_user_data: HashMap<String, HashMap<String, String>>,
}
impl TWData {
fn try_to_create_instance() -> CommonResult<TWData> {
let mut tw_data = TWData::default();
tw_data.set_size(terminal_lib_operations::lookup_size()?);
Ok(tw_data)
}
pub fn set_size(&mut self, new_size: Size) {
self.size = new_size;
self.dump_state_to_log("main_event_loop -> Resize");
}
pub fn get_size(&self) -> Size { self.size }
pub fn dump_state_to_log(&self, msg: &str) {
call_if_true!(DEBUG_TUI_MOD, log_no_err!(INFO, "{} -> {:?}", msg, self));
}
}
pub struct TerminalWindow;
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: Default + Clone + PartialEq + Debug + Sync + Send + 'static,
A: Default + Clone + Sync + Send + 'static,
{
let _tw_data = TWData::try_to_create_instance()?;
let shared_tw_data: SharedTWData = Arc::new(RwLock::new(_tw_data));
let my_raw_mode = RawMode::start(&shared_tw_data).await;
let shared_store: SharedStore<S, A> = Arc::new(RwLock::new(store));
let _subscriber = AppManager::new_box(&shared_app, &shared_store, &shared_tw_data);
shared_store.write().await.add_subscriber(_subscriber).await;
let mut async_event_stream = AsyncEventStream::default();
AppManager::render_app(&shared_store, &shared_app, &shared_tw_data, None).await?;
shared_tw_data
.read()
.await
.dump_state_to_log("main_event_loop -> Startup 🚀");
loop {
let maybe_input_event = async_event_stream.try_to_get_input_event().await;
let input_event = match maybe_input_event {
Some(it) => it,
_ => continue,
};
call_if_true!(
DEBUG_TUI_MOD,
log_no_err!(INFO, "main_event_loop -> Tick: ⏰ {}", input_event)
);
if let InputEvent::Resize(new_size) = input_event {
shared_tw_data.write().await.set_size(new_size);
shared_tw_data.write().await.maybe_render_pipeline = None;
AppManager::render_app(&shared_store, &shared_app, &shared_tw_data, None).await?;
}
let propagation_result_from_app =
TerminalWindow::process_input_event(&shared_tw_data, &shared_store, &shared_app, &input_event).await?;
match propagation_result_from_app {
EventPropagation::Propagate => {
if let Continuation::Exit = DefaultInputEventHandler::no_consume(input_event, &exit_keys).await {
break;
};
}
EventPropagation::ConsumedRerender => {
AppManager::render_app(&shared_store, &shared_app, &shared_tw_data, None).await?;
}
EventPropagation::Consumed => {}
}
}
my_raw_mode.end(&shared_tw_data).await;
Ok(())
}
async fn process_input_event<S, A>(
shared_tw_data: &SharedTWData,
shared_store: &SharedStore<S, A>,
shared_app: &SharedApp<S, A>,
input_event: &InputEvent,
) -> CommonResult<EventPropagation>
where
S: Default + Clone + PartialEq + Debug + Sync + Send + 'static,
A: Default + Clone + Sync + Send + 'static,
{
let propagation_result_from_app = match SPAWN_PROCESS_INPUT {
true => {
let propagation_result_from_app = {
let shared_tw_data_clone = shared_tw_data.clone();
let shared_store_clone = shared_store.clone();
let shared_app_clone = shared_app.clone();
let input_event_clone = input_event.clone();
let join_handle = tokio::spawn(async move {
AppManager::route_input_to_app(
&shared_tw_data_clone,
&shared_store_clone,
&shared_app_clone,
&input_event_clone,
)
.await
});
join_handle.await??
};
call_if_true!(
DEBUG_TUI_MOD,
log_no_err!(
INFO,
"main_event_loop -> 🚥 SPAWN propagation_result_from_app: {:?}",
propagation_result_from_app
)
);
propagation_result_from_app
}
false => {
let propagation_result_from_app =
AppManager::route_input_to_app(shared_tw_data, shared_store, shared_app, input_event).await?;
call_if_true!(
DEBUG_TUI_MOD,
log_no_err!(
INFO,
"main_event_loop -> 🚥 NO_SPAWN propagation_result_from_app: {:?}",
propagation_result_from_app
)
);
propagation_result_from_app
}
};
Ok(propagation_result_from_app)
}
}
struct AppManager<S, A>
where
S: Default + Clone + PartialEq + Debug + Sync + Send + 'static,
A: Default + Clone + Sync + Send + 'static,
{
shared_app: SharedApp<S, A>,
shared_store: SharedStore<S, A>,
shared_tw_data: SharedTWData,
}
#[async_trait]
impl<S, A> AsyncSubscriber<S> for AppManager<S, A>
where
S: Default + Clone + PartialEq + Debug + Sync + Send + 'static,
A: Default + Clone + Sync + Send,
{
async fn run(&self, my_state: S) {
let result = AppManager::render_app(
&self.shared_store,
&self.shared_app,
&self.shared_tw_data,
my_state.into(),
)
.await;
if let Err(e) = result {
call_if_true!(DEBUG_TUI_MOD, log_no_err!(ERROR, "MySubscriber::run -> Error: {}", e))
}
}
}
impl<S, A> AppManager<S, A>
where
S: Default + Clone + PartialEq + Debug + Sync + Send + 'static,
A: Default + Clone + Sync + Send,
{
fn new_box(
shared_app: &SharedApp<S, A>,
shared_store: &SharedStore<S, A>,
shared_tw_data: &SharedTWData,
) -> Box<Self> {
Box::new(AppManager {
shared_app: shared_app.clone(),
shared_store: shared_store.clone(),
shared_tw_data: shared_tw_data.clone(),
})
}
pub async fn route_input_to_app(
shared_tw_data: &SharedTWData,
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_tw_data.read().await.get_size();
let global_scope_args = GlobalScopeArgs {
shared_tw_data,
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_tw_data: &SharedTWData,
maybe_state: Option<S>,
) -> CommonResult<()> {
throws!({
let window_size = shared_tw_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_tw_data,
window_size: &window_size,
};
let render_result: CommonResult<RenderPipeline> =
if window_size.is_too_small_to_display(MinSize::Col.int_value(), MinSize::Row.int_value()) {
shared_tw_data.write().await.maybe_render_pipeline = 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();
call_if_true!(
DEBUG_TUI_MOD,
log_no_err!(ERROR, "MySubscriber::render() error ❌: {}", error)
);
}
Ok(render_pipeline) => {
render_pipeline.paint(FlushKind::ClearBeforeFlush, shared_tw_data).await;
call_if_true!(DEBUG_TUI_MOD, {
log_no_err!(
INFO,
"🎨 MySubscriber::paint() ok ✅: \n size: {:?}\n state: {:?}\n",
window_size,
state,
);
});
}
}
});
}
}
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.int_value(),
MinSize::Row.int_value()
));
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.rows / 2;
let col_pos = (window_size.cols - trunc_display_msg_len) / 2;
render_pipeline!(@new ZOrder::Normal =>
RenderOp::ResetColor,
RenderOp::MoveCursorPositionAbs(position! {col: col_pos, row: row_pos}),
RenderOp::SetFgColor(TWColor::DarkRed),
RenderOp::PrintTextWithAttributes(
lolcat_each_char_in_unicode_string(&trunc_display_msg, None),
Some(style! {attrib: [bold]}))
)
}