zero-trust-rps 0.0.5

Online Multiplayer Rock Paper Scissors
Documentation
use std::io::{stderr, IsTerminal as _, Stdout};
use std::pin::pin;
use std::time::Duration;

use crossterm::event::{KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
use futures::pin_mut;
use quinn::rustls::crypto::aws_lc_rs::default_provider;
use quinn::VarInt;
use ratatui::layout::Position;
use ratatui::prelude::{Backend, CrosstermBackend};
use ratatui::Terminal;
use tokio::sync::mpsc::unbounded_channel;

use super::app::App;
use super::event::{Event, EventHandler};
use super::handlers::{handle_confirm_action, handle_key_events};
use super::tui::Tui;
use crate::cli_utils::configure_logging;
use crate::common::client::connection::conn::run_client;
use crate::common::result::DynResult;

use super::ClientOptions;

pub async fn main(module: &'static str, options: &ClientOptions) -> DynResult<()> {
    default_provider()
        .install_default()
        .expect("Installing default provider should work!");
    // TODO: if bot
    // configure_logging(module)?;
    // tokio::spawn(run_bot(1, options.domain.clone(), options.port, options.ip));

    if !stderr().is_terminal() {
        configure_logging(module)?;
    } else {
        // TODO: log to file per default??
        eprintln!("Redirect stderr to a file to activate logging.");
    }

    let (connection, writer, reader) = super::init::initialize_connection(options).await?;

    let (umove_send, umove_recv) = unbounded_channel();

    // Create an application.
    let mut app = pin!(App::new(umove_send, options));

    // Initialize the terminal user interface.
    let backend = CrosstermBackend::new(std::io::stdout());
    let terminal = Terminal::new(backend)?;
    let events = EventHandler::new(Duration::from_millis(250));

    let conn_fut = tokio::spawn(run_client(
        options.get_timeout(),
        writer,
        reader,
        events.get_sender(),
        events.get_sender(),
        umove_recv,
    ));

    let mut tui = Tui::new(terminal, events);
    tui.init()?;

    let main_loop_fut = async {
        let res = run_main_loop(&mut app, &mut tui).await;
        tui.exit()?;
        res
    };

    pin_mut!(main_loop_fut);

    let result = futures::future::select(main_loop_fut, conn_fut).await;

    // Exit the user interface.
    // tui.exit()?;
    // TODO: SHOW CURSOR AGAIN LIKE tui.exit()
    Tui::<CrosstermBackend<Stdout>>::reset()?;

    connection.close(VarInt::from_u32(1), b"Exiting");

    use futures::future::Either::*;

    let _: () = match result {
        Left((res, _)) => match res {
            Ok(()) => (),
            Err(err) => {
                println!("{err}");
                Err(err)?;
            }
        },
        Right((res, _)) => {
            println!("Disconnected");
            log::info!("Disconnected");
            res??
        }
    };
    Ok(())
}

#[inline]
async fn run_main_loop<B: Backend>(app: &mut App, tui: &mut Tui<B>) -> DynResult<()> {
    log::debug!("run_main_loop on thread {:?}", std::thread::current().id());

    // Start the main loop.
    while app.running {
        // Render the user interface.
        tui.draw(app)?;
        // Handle events.
        match tui.events.next().await.ok_or("Events channel closed")? {
            Event::Tick => app.tick(),
            Event::Key(key_event) => handle_key_events(key_event, app).await?,
            Event::Mouse(MouseEvent {
                kind: MouseEventKind::Up(MouseButton::Left),
                column: c,
                row: r,
                modifiers: KeyModifiers::NONE,
            }) => {
                if let Some(button) = app.confirm_button {
                    if button.contains(Position::new(c, r)) {
                        handle_confirm_action(app).await?;
                    }
                }
                if let Some(pop_up) = app.pop_up {
                    if !pop_up.contains(Position::new(c, r)) {
                        app.close_err_pop_up();
                    }
                }
            }
            Event::Mouse(_) => {}
            Event::Resize(_, _) => {}
            Event::ServerEvent(evt) => {
                use super::event::ServerEvent::*;
                match evt {
                    Other => (),
                    Error(error) => {
                        if let Some(old) = app.submit_error.as_mut() {
                            old.push('\n');
                            old.push_str(&error);
                        } else {
                            app.submit_error = Some(error);
                        }
                        app.user_tried_to_change_state_already = false;
                    }
                }
            }
            Event::ClientStateUpdated(client_state) => {
                if client_state.timed_out {
                    return Err("Timed out!".into());
                }
                if app.client_state.state != client_state.state {
                    app.user_tried_to_change_state_already = false;
                }
                if app.client_state.current_plays != client_state.current_plays {
                    if let Some(curr_play) = client_state.current_plays.as_ref() {
                        app.old_plays.push(curr_play.clone());
                    }
                }

                app.client_state = client_state;
            }
        }
    }

    Ok(())
}