voynich 0.1.1

Library for creating anonymous, end-to-end encrypted and authenticated chat applications
Documentation
use anyhow::{Context, Result};
use crossterm::event::{Event, EventStream, KeyCode, KeyEvent, KeyModifiers};
use futures::{
    stream::{FusedStream, Stream},
    task::Poll,
    StreamExt,
};
use futures_lite::StreamExt as LiteStreamExt;
use std::pin::Pin;
use std::task::Context as TaskContext;
use tokio::select;
use tor_client_lib::{control_connection::OnionServiceListener, TorServiceId};
use voynich::{
    engine::{ConnectionDirection, Engine, NetworkEvent},
    logger::{Logger, StandardLogger},
};

use crate::{
    app_context::{AppContext, ConnectionContext},
    input::{
        allow_connection_input::AllowConnectionInput, chat_input::ChatInput,
        command_input::CommandInput,
    },
    root::Root,
    term::Term,
};

#[derive(Debug)]
pub struct TermInputStream {
    reader: EventStream,
}

impl TermInputStream {
    fn new() -> Self {
        Self {
            reader: EventStream::new(),
        }
    }
}

impl Stream for TermInputStream {
    type Item = Result<Event, std::io::Error>;

    fn poll_next(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<Option<Self::Item>> {
        self.reader.poll_next(cx)
    }
}

impl FusedStream for TermInputStream {
    fn is_terminated(&self) -> bool {
        false
    }
}

pub trait InputHandler {
    async fn handle_input_event(
        &mut self,
        event: Event,
        context: &mut AppContext,
        engine: &mut Engine,
        logger: &mut StandardLogger,
    );
}

#[derive(Debug)]
pub struct App {
    term: Term,
    input_stream: TermInputStream,
    context: AppContext,
    chat_input: ChatInput,
    command_input: CommandInput,
    allow_connection_input: AllowConnectionInput,
}

impl App {
    fn new(id: TorServiceId, onion_service_address: String) -> Result<Self> {
        Ok(Self {
            term: Term::start()?,
            input_stream: TermInputStream::new(),
            context: AppContext::new(id, onion_service_address),
            chat_input: ChatInput::new(),
            command_input: CommandInput::new(),
            allow_connection_input: AllowConnectionInput::new(),
        })
    }

    pub async fn run(
        engine: &mut Engine,
        listener: &OnionServiceListener,
        logger: &mut StandardLogger,
    ) -> Result<()> {
        install_panic_hook();
        let mut app = Self::new(engine.id(), engine.onion_service_address())?;

        logger.log_info(&format!(
            "Onion service {} in service",
            engine.onion_service_address(),
        ));

        logger.log_info("NOTE: To bring up the help screen, type ctrl-h");

        while !app.context.should_quit {
            app.draw(logger)?;
            app.handle_events(engine, listener, logger).await?;
        }
        Term::stop()?;
        Ok(())
    }

    fn draw(&mut self, logger: &mut StandardLogger) -> Result<()> {
        self.term
            .draw(|frame| {
                let mut root =
                    Root::new(&self.context, logger, &self.command_input, &self.chat_input);
                if let Some((x, y)) = root.get_cursor_location(frame.size()) {
                    frame.set_cursor(x, y);
                }
                frame.render_widget(root, frame.size());
            })
            .context("terminal.draw")?;
        Ok(())
    }

    async fn handle_events(
        &mut self,
        engine: &mut Engine,
        listener: &OnionServiceListener,
        logger: &mut StandardLogger,
    ) -> Result<()> {
        select! {
            result = self.input_stream.select_next_some() => {
                match result {
                    Ok(event) => {
                        self.handle_input_event(event, engine, logger).await;
                        Ok(())
                    },
                    Err(error) => {
                        logger.log_error(&format!("Error reading input: {}", error));
                        Ok(())
                    },
                }
            }
            result = engine.get_event(logger) => {
                match result {
                    Ok(Some(NetworkEvent::NewConnection(connection))) => {
                        if *connection.direction() == ConnectionDirection::Incoming {
                            self.context.connection_context = Some(ConnectionContext::new(&connection.id()));
                        } else {
                            self.context.add_new_chat(&connection.id());
                        }
                        Ok(())
                    }
                    Ok(Some(NetworkEvent::Message(chat_message))) => {
                        if let Some(chat) = self.context.chats.get_mut(&chat_message.sender) {
                            chat.add_message(*chat_message);
                        }
                        Ok(())
                    },
                    Ok(Some(NetworkEvent::ConnectionClosed(connection))) => {
                        self.context.chat_list.remove(&connection.id());
                        self.context.chats.remove(&connection.id());
                        self.context.remove_id(&connection.id());
                        Ok(())
                    }
                    Ok(None) => Ok(()),
                    Err(error) => Err(error),
                }
            }
            result = listener.accept() => {
                match result {
                    Ok((stream, socket_addr)) => {
                        engine.handle_incoming_connection(stream, socket_addr).await;
                    },
                    Err(error) => {
                        logger.log_error(&format!("Error in accept: {}", error));
                    }
                }
                Ok(())
            }
        }
    }

    async fn handle_input_event(
        &mut self,
        event: Event,
        engine: &mut Engine,
        logger: &mut StandardLogger,
    ) {
        if self.context.connection_context.is_some() {
            self.allow_connection_input
                .handle_input_event(event, &mut self.context, engine, logger)
                .await;
        } else if self.context.show_command_popup {
            self.command_input
                .handle_input_event(event, &mut self.context, engine, logger)
                .await;
        } else if self.context.chat_list.current_index().is_some() {
            self.chat_input
                .handle_input_event(event, &mut self.context, engine, logger)
                .await;
        } else if let Event::Key(KeyEvent {
            code,
            modifiers,
            kind: _,
            state: _,
        }) = event
        {
            match code {
                KeyCode::Char(character) => {
                    if character == 'c' && modifiers.contains(KeyModifiers::CONTROL) {
                        self.context.should_quit = true;
                    } else if character == 'k' && modifiers.contains(KeyModifiers::CONTROL) {
                        self.context.show_command_popup = !self.context.show_command_popup;
                        if self.context.show_command_popup {
                            self.context.show_welcome_popup = false;
                        }
                    } else if character == 'h' && modifiers.contains(KeyModifiers::CONTROL) {
                        self.context.show_welcome_popup = !self.context.show_welcome_popup;
                        if self.context.show_welcome_popup {
                            self.context.show_command_popup = false;
                        }
                    }
                }
                KeyCode::Esc => {
                    if self.context.show_welcome_popup {
                        self.context.show_welcome_popup = false;
                    }
                    if self.context.show_command_popup {
                        self.context.show_command_popup = false;
                    }
                }
                _ => {}
            }
        }
    }
}

pub fn install_panic_hook() {
    better_panic::install();
    let hook = std::panic::take_hook();
    std::panic::set_hook(Box::new(move |info| {
        let _ = Term::stop();
        hook(info);
        std::process::exit(1);
    }));
}