use crate::component_name::ComponentName::Prompt;
use crate::{
action::Action, app_context::AppContext, app_error::AppError,
configs::custom::keymap_custom::ActionBinding, event::Event, tg::tg_backend::TgBackend,
tui::Tui, tui_backend::TuiBackend,
};
use ratatui::layout::Rect;
use std::{collections::HashMap, io, sync::Arc, time::Instant};
use tdlib_rs::enums::ChatList;
use tokio::sync::mpsc::UnboundedSender;
pub async fn run_app(
app_context: Arc<AppContext>,
tui: &mut Tui,
tui_backend: &mut TuiBackend,
tg_backend: &mut TgBackend,
) -> Result<(), AppError<Action>> {
tracing::info!("Starting run_app");
io::Write::write_all(&mut io::stdout().lock(), b"\x1b[2J\x1b[1;1H").unwrap();
tg_backend.start();
tg_backend.set_logging().await;
tg_backend.handle_authorization_state().await;
tg_backend.use_quick_ack().await;
tg_backend.get_me().await;
tg_backend.load_chats(ChatList::Main, 30).await;
match handle_cli(Arc::clone(&app_context), tg_backend).await {
HandleCliOutcome::Quit => {
futures::join!(quit_cli(tg_backend));
return Ok(());
}
HandleCliOutcome::Logout => {
futures::join!(log_out(tg_backend));
return Ok(());
}
HandleCliOutcome::Continue => {}
}
tg_backend.online().await;
tg_backend.disable_animated_emoji(true).await;
tui_backend.enter()?;
tui.register_action_handler(app_context.action_tx().clone())?;
while tg_backend.have_authorization {
handle_tui_backend_events(Arc::clone(&app_context), tui, tui_backend).await?;
handle_tg_backend_events(Arc::clone(&app_context), tg_backend).await?;
handle_app_actions(Arc::clone(&app_context), tui, tui_backend, tg_backend).await?;
if app_context.quit_acquire() {
quit_tui(tg_backend, tui_backend).await;
tracing::info!("Quitting");
return Ok(());
}
}
Ok(())
}
async fn handle_tg_backend_events(
app_context: Arc<AppContext>,
tg_backend: &mut TgBackend,
) -> Result<(), AppError<Action>> {
if let Some(event) = tg_backend.next().await {
match event {
Event::LoadChats(chat_list, limit) => {
app_context
.action_tx()
.send(Action::LoadChats(chat_list, limit))?;
}
Event::SendMessage(message, reply_to) => {
app_context
.action_tx()
.send(Action::SendMessage(message, reply_to))?;
}
Event::SendMessageEdited(message_id, message) => {
app_context
.action_tx()
.send(Action::SendMessageEdited(message_id, message))?;
}
Event::GetChatHistory => {
app_context.action_tx().send(Action::GetChatHistory)?;
}
Event::DeleteMessages(message_ids, revoke) => {
app_context
.action_tx()
.send(Action::DeleteMessages(message_ids, revoke))?;
}
Event::EditMessage(message_id, message) => {
app_context
.action_tx()
.send(Action::FocusComponent(Prompt))?;
app_context
.action_tx()
.send(Action::EditMessage(message_id, message))?;
}
Event::ReplyMessage(message_id, message) => {
app_context
.action_tx()
.send(Action::FocusComponent(Prompt))?;
app_context
.action_tx()
.send(Action::ReplyMessage(message_id, message))?;
}
Event::ViewAllMessages => {
app_context.action_tx().send(Action::ViewAllMessages)?;
}
_ => {}
}
}
Ok(())
}
#[allow(clippy::await_holding_lock)]
async fn handle_tui_backend_events(
app_context: Arc<AppContext>,
tui: &mut Tui,
tui_backend: &mut TuiBackend,
) -> Result<(), AppError<Action>> {
if let Some(event) = tui_backend.next().await {
match event {
Event::Render => app_context.action_tx().send(Action::Render)?,
Event::Resize(width, height) => app_context
.action_tx()
.send(Action::Resize(width, height))?,
Event::Key(key, modifiers) => {
app_context
.action_tx()
.send(Action::from_key_event(key, modifiers))?;
if let Some(action_binding) = app_context
.keymap_config()
.core_window
.get(&Event::Key(key, modifiers))
{
match action_binding {
ActionBinding::Single { action, .. } => {
app_context.action_tx().send(action.clone())?;
return Ok(());
}
ActionBinding::Multiple(map_event_action) => {
consume_until_single_action(
&app_context.action_tx(),
tui_backend,
map_event_action.clone(),
)
.await;
return Ok(());
}
}
}
}
Event::Paste(ref text) => app_context.action_tx().send(Action::Paste(text.clone()))?,
_ => {}
}
if let Some(action) = tui.handle_events(Some(event.clone()))? {
app_context.action_tx().send(action)?
}
}
Ok(())
}
async fn consume_until_single_action(
action_tx: &UnboundedSender<Action>,
tui_backend: &mut TuiBackend,
map_event_action: HashMap<Event, ActionBinding>,
) {
let start = Instant::now();
loop {
if let Some(event) = tui_backend.next().await {
if let Some(ActionBinding::Single { action, .. }) = map_event_action.get(&event) {
action_tx.send(action.clone()).unwrap();
break;
}
}
if start.elapsed().as_secs() > 1 {
break;
}
}
}
#[allow(clippy::await_holding_lock)]
pub async fn handle_app_actions(
app_context: Arc<AppContext>,
tui: &mut Tui,
tui_backend: &mut TuiBackend,
tg_backend: &mut TgBackend,
) -> Result<(), AppError<Action>> {
while let Ok(action) = app_context.action_rx().try_recv() {
match action {
Action::Render => {
tui_backend.terminal.draw(|f| {
tui.draw(f, f.size()).unwrap();
})?;
}
Action::Resize(width, height) => {
tui_backend
.terminal
.resize(Rect::new(0, 0, width, height))?;
tui_backend.terminal.draw(|f| {
tui.draw(f, f.size()).unwrap();
})?;
}
Action::Quit => {
app_context.quit_store(true);
}
Action::LoadChats(chat_list, limit) => {
tg_backend.load_chats(chat_list.into(), limit).await;
}
Action::SendMessage(ref message, ref reply_to) => {
let _ = tg_backend
.send_message(
message.to_string(),
app_context.tg_context().open_chat_id(),
reply_to.clone(),
)
.await;
}
Action::SendMessageEdited(message_id, ref message) => {
tg_backend
.send_message_edited(message_id, message.to_string())
.await;
}
Action::GetChatHistory => {
tg_backend
.get_chat_history(app_context.tg_context().open_chat_id())
.await;
}
Action::DeleteMessages(ref message_ids, revoke) => {
tg_backend
.delete_messages(
app_context.tg_context().open_chat_id(),
message_ids.to_vec(),
revoke,
)
.await;
}
Action::ReplyMessage(message_id, ref message) => {
app_context
.tg_context()
.set_reply_message(message_id, message.to_string());
}
Action::ViewAllMessages => {
tg_backend.view_all_messages().await;
}
_ => {}
}
tui.update(action.clone())
}
Ok(())
}
enum HandleCliOutcome {
Quit,
Continue,
Logout,
}
#[allow(clippy::await_holding_lock)]
async fn handle_cli(app_context: Arc<AppContext>, tg_backend: &mut TgBackend) -> HandleCliOutcome {
if app_context.cli_args().telegram_cli().logout() {
return HandleCliOutcome::Logout;
}
if let Some(chat) = app_context.cli_args().telegram_cli().send_message() {
futures::join!(tg_backend.load_all_chats());
let [chat_name, message_text] = chat.as_slice() else {
tracing::error!("Invalid number of arguments for send message");
println!("Invalid number of arguments for send message");
return HandleCliOutcome::Quit;
};
match tg_backend.search_chats(chat_name.to_string()).await {
Ok(chat) => {
tracing::info!("Chat found: {:?}", chat);
let total_chats = chat.total_count;
let chats_vec = chat.chat_ids;
if total_chats == 0 {
tracing::error!("No chat found with the name: {}", chat_name);
println!("No chat found with the name: {}", chat_name);
return HandleCliOutcome::Quit;
}
if total_chats > 1 {
tracing::error!("Multiple chats found with the name: {}", chat_name);
println!("Multiple chats found with the name: {}", chat_name);
return HandleCliOutcome::Quit;
}
let chat_id = chats_vec[0];
let msg = tg_backend
.send_message(message_text.to_string(), chat_id, None)
.await;
match msg {
Ok(msg) => {
let message_id = msg.id;
while app_context.tg_context().last_acknowledged_message_id() != message_id
{
}
}
Err(e) => {
tracing::error!("Error sending message: {:?}", e);
println!("Error sending message: {:?}", e);
return HandleCliOutcome::Quit;
}
}
tracing::info!(
"Sent message {} to chat_name {} ({})",
message_text,
chat_name,
chat_id
);
return HandleCliOutcome::Quit;
}
Err(e) => {
tracing::error!("Error searching for chat: {:?}", e);
println!("Error searching for chat: {:?}", e);
return HandleCliOutcome::Quit;
}
}
}
HandleCliOutcome::Continue
}
async fn quit_tui(tg_backend: &mut TgBackend, tui_backend: &mut TuiBackend) {
futures::join!(tg_backend.offline());
tg_backend.have_authorization = false;
tg_backend.close().await;
tui_backend.exit();
tg_backend.handle_authorization_state().await;
io::Write::write_all(&mut io::stdout().lock(), b"\x1b[2J\x1b[1;1H").unwrap();
}
async fn quit_cli(tg_backend: &mut TgBackend) {
tg_backend.have_authorization = false;
tg_backend.close().await;
tg_backend.handle_authorization_state().await;
io::Write::write_all(&mut io::stdout().lock(), b"\x1b[2J\x1b[1;1H").unwrap();
}
async fn log_out(tg_backend: &mut TgBackend) {
tg_backend.log_out().await;
tg_backend.handle_authorization_state().await;
io::Write::write_all(&mut io::stdout().lock(), b"\x1b[2J\x1b[1;1H").unwrap();
}