use rat_focus::Focus;
use std::time::Instant;
use ratatui::DefaultTerminal;
use tokio_util::sync::CancellationToken;
use tokio::sync::mpsc::UnboundedReceiver;
use crossterm::event::{KeyCode, KeyEvent};
use crate::{
server,
cli::{Cli, Commands, SignalingSolutions},
client::{
client_init::init,
signaling::{negotiator::HandshakeState, signaling_manual::SignalingManual},
},
ui::{
theme::Theme,
utils::{CombinedWidgetState, Shortcut},
widgets::{
files_widget::FileListWidgetState, history_widget::HistoryWidgetState, users_widget::UserListWidgetState,
manual_handshake_widget::ManualHandshakeWidgetState, rooms_widget::RoomListWidgetState,throbber::ThrobberStateCounter,
},
},
app::{
app_event::AppEvent,
file_manager::FileManager,
event::{BasicEvent, EventHandler},
models::{ClientState, ErrorTX, Maid},
handlers::{app_handler::AppHandler, client_handler::ClientHandler, server_handler::ServerHandler},
},
};
pub struct App {
pub exit: bool,
pub redraw: bool,
pub last_redraw: Instant,
pub args: Cli,
pub events: EventHandler,
pub error: Option<color_eyre::Report>,
pub error_tx: ErrorTX,
pub error_rx: UnboundedReceiver<color_eyre::Report>,
pub cancellation_token: CancellationToken,
pub theme: Theme,
pub file_manager: FileManager,
pub client_state: ClientState,
pub handshake_state: HandshakeState,
pub focus: Focus,
pub throbber_sc: ThrobberStateCounter,
pub widget_shortcuts: Vec<Shortcut>,
pub handshake_widget_state: ManualHandshakeWidgetState,
pub input_list_widget_state: FileListWidgetState,
pub output_list_widget_state: FileListWidgetState,
pub room_list_widget_state: RoomListWidgetState,
pub user_list_widget_state: UserListWidgetState,
pub history_widget_state: HistoryWidgetState,
}
impl App {
pub fn new(args: Cli) -> color_eyre::Result<Self> {
let (error_tx, error_rx) = tokio::sync::mpsc::unbounded_channel::<color_eyre::Report>();
let ignore_empty: bool = if let Commands::Client(client_args) = &args.app_mode {
client_args.ignore_empty
} else {
false
};
Ok(Self {
exit: false,
redraw: true,
last_redraw: Instant::now(),
args,
events: EventHandler::new(),
error: None,
error_tx: ErrorTX(error_tx),
error_rx,
theme: Theme::load_default()?,
file_manager: FileManager::new(ignore_empty),
client_state: ClientState::default(),
handshake_state: HandshakeState::default(),
cancellation_token: CancellationToken::new(),
focus: Focus::default(),
throbber_sc: ThrobberStateCounter::new(3),
widget_shortcuts: vec![],
handshake_widget_state: ManualHandshakeWidgetState::default(),
input_list_widget_state: FileListWidgetState::default(),
output_list_widget_state: FileListWidgetState::default(),
room_list_widget_state: RoomListWidgetState::default(),
user_list_widget_state: UserListWidgetState::default(),
history_widget_state: HistoryWidgetState::default(),
})
}
pub fn get_maid(&self) -> Maid {
Maid::new(
self.error_tx.clone(),
self.events.sender(),
self.cancellation_token.child_token(),
)
}
pub async fn run(
mut self,
args: &Cli,
terminal: &mut DefaultTerminal,
) -> color_eyre::Result<()> {
startup(&mut self, args)?;
self.main_loop(terminal).await?; self.cancellation_token.cancel(); self.error_loop(terminal).await?;
if let Some(error) = self.error { Err(error) } else { Ok(()) }
}
async fn main_loop(&mut self, terminal: &mut DefaultTerminal) -> color_eyre::Result<()> {
while !self.exit { self.try_redraw(terminal)?; let error = tokio::select! {
event = self.events.next() => { let result = self.process_event(event?).await;
result.err()
}
err = self.error_rx.recv() => { err
}
};
if let Some(err) = error {
log::error!("{}", err);
self.error = Some(err);
self.exit = true;
}
}
Ok(())
}
async fn error_loop(&mut self, terminal: &mut DefaultTerminal) -> color_eyre::Result<()> {
if self.error.is_some() {
loop { self.try_redraw(terminal)?; let event = self.events.next().await?;
match event {
BasicEvent::Tick => { self.on_tick(); },
BasicEvent::Crossterm(crossterm::event::Event::Key(key_event)) => {
if key_event.is_release()
&& let KeyCode::Char('q') = key_event.code{
break;
}
}
_ => {}
}
}
}
Ok(())
}
async fn process_event(&mut self, event: BasicEvent) -> color_eyre::Result<()> {
self.handle_tick_and_crossterm(&event)?;
if let BasicEvent::App(app_event) = event {
self.queue_redraw();
match self.args.app_mode {
Commands::Client(_) => ClientHandler::handle_app_events(self, app_event)?,
Commands::Server(_) => ServerHandler::handle_app_events(self, app_event)?,
}
}
Ok(())
}
fn handle_tick_and_crossterm(&mut self, event: &BasicEvent) -> color_eyre::Result<()> {
match event {
BasicEvent::Tick => self.on_tick(),
BasicEvent::Crossterm(crossterm::event::Event::Key(key_event)) => {
let mut app_events: Vec<AppEvent> = vec![];
self.handle_focus_key_events(key_event);
let handler_event = match self.args.app_mode {
Commands::Client(_) => ClientHandler::handle_key_events(key_event)?,
Commands::Server(_) => ServerHandler::handle_key_events(key_event)?,
};
app_events.push(handler_event);
for cws in self.get_focusable_widgets() {
if cws.is_focused() {
let widget_event = cws.handle_key_events(key_event)?;
app_events.push(widget_event);
}
}
if !app_events.is_empty() { self.queue_redraw(); }
for ev in app_events { self.events.send_app_event(ev); } }
_ => {}
}
Ok(())
}
fn handle_focus_key_events(&mut self, key_event: &KeyEvent) {
if key_event.is_release() {
match key_event.code {
KeyCode::Esc => { self.focus.none(); },
KeyCode::Tab => { self.focus.next(); },
KeyCode::BackTab => { self.focus.prev(); },
_ => {}
};
}
}
fn draw(&mut self, terminal: &mut DefaultTerminal) -> color_eyre::Result<()> {
self.set_shortcuts(); terminal.draw(|frame| frame.render_widget(self, frame.area()))?; Ok(())
}
fn try_redraw(&mut self, terminal: &mut DefaultTerminal) -> color_eyre::Result<()> {
if self.redraw
&& self.last_redraw.elapsed().as_millis() >= 33 {
self.redraw = false;
self.draw(terminal)?;
self.last_redraw = Instant::now();
}
Ok(())
}
fn queue_redraw(&mut self) { self.redraw = true; }
fn set_shortcuts(&mut self) {
let mut shortcuts: Vec<Shortcut> = vec![];
for cws in self.get_focusable_widgets() {
if cws.is_focused() { shortcuts = cws.get_shortcuts(); }
}
self.widget_shortcuts = shortcuts;
}
pub fn on_tick(&mut self) {
self.throbber_sc.update();
}
pub fn focusable_widgets_client(&mut self) -> Vec<Box<&mut dyn CombinedWidgetState>> {
vec![
Box::new(&mut self.handshake_widget_state),
Box::new(&mut self.input_list_widget_state),
Box::new(&mut self.output_list_widget_state),
]
}
pub fn focusable_widgets_server(&mut self) -> Vec<Box<&mut dyn CombinedWidgetState>> {
vec![
Box::new(&mut self.room_list_widget_state),
Box::new(&mut self.user_list_widget_state),
Box::new(&mut self.history_widget_state),
]
}
pub fn get_focusable_widgets(&mut self) -> Vec<Box<&mut dyn CombinedWidgetState>> {
match self.args.app_mode {
Commands::Client(_) => self.focusable_widgets_client(),
Commands::Server(_) => self.focusable_widgets_server(),
}
}
}
fn startup(app: &mut App, args: &Cli) -> color_eyre::Result<()> {
if let Commands::Client(client_args) = &app.args.app_mode {
log::info!("Client started in {:?} mode", client_args.signaling_mode);
}
match &args.app_mode {
Commands::Client(args) => {
let maid = app.get_maid();
let args_client = args.clone();
if let Some(files) = args.files.clone() {
app.file_manager.add_output_files(&files)?;
}
let mut signaling_manual: Option<SignalingManual> = None;
if let SignalingSolutions::Manual(args) = &args.signaling_mode {
let sm = SignalingManual::new(app.events.sender(), args.clone());
app.client_state.handshake_tx = Some(sm.sender());
signaling_manual = Some(sm);
}
tokio::spawn(async move {
let token: CancellationToken = maid.token.child_token();
let error_tx = maid.error_tx.clone();
tokio::select! {
_ = token.cancelled() => {},
result = init(maid, signaling_manual, args_client) => {
if let Err(err) = result { error_tx.send_error(err); }
},
}
});
}
Commands::Server(args) => {
let maid = app.get_maid();
let args = args.clone();
tokio::spawn(async move {
let token = maid.token.child_token();
let error_tx = maid.error_tx.clone();
tokio::select! {
_ = token.cancelled() => {},
result = server::signal::main(maid, args) => {
if let Err(err) = result { error_tx.send_error(err); }
},
}
});
}
}
Ok(())
}