glyph_ui 0.1.0

TUI library utilizing the Elm architecture
Documentation
use std::{io::Write, pin::Pin};

use crossterm::{
    cursor::{Hide, Show},
    event::{Event as CEvent, EventStream},
    execute,
    terminal::{
        disable_raw_mode, enable_raw_mode, size, Clear, ClearType,
        EnterAlternateScreen, LeaveAlternateScreen,
    },
    ExecutableCommand, QueueableCommand,
};
use futures::{channel::mpsc, prelude::*};

use crate::{
    event::{Cause, Event},
    Am, Amv, CommandBuf, Gui, Printer, View,
};

/// Event loop control flow
#[derive(PartialEq, Eq)]
pub enum ControlFlow {
    /// Pause the event loop until new events are available
    Wait,

    /// Shut down the event loop
    Exit,
}

/// Runtime for a Glyph User Interface application
pub struct Runtime<T, A> {
    events_tx: mpsc::Sender<Event<T>>,
    events_rx: mpsc::Receiver<Event<T>>,
    app: A,
}

impl<T, A> Runtime<T, A>
where
    T: Send + 'static,
    A: Gui<Event = T>,
{
    /// Create a new runtime
    ///
    /// `spawn` must be be able to spawn a future on the async runtime being
    /// used. For example, `|task| tokio::spawn(task)` will work.
    pub fn new<S, H>(app: A, spawn: S) -> Self
    where
        S: FnOnce(Pin<Box<dyn Future<Output = ()> + Send>>) -> H,
    {
        let (events_tx, events_rx) = mpsc::channel(16);

        let task = {
            let events_tx = events_tx.clone();

            async {
                EventStream::new()
                    .map(|e| match e {
                        Ok(CEvent::Resize(w, h)) => {
                            Ok(Event::Resize((w, h).into()))
                        }
                        Ok(CEvent::Key(key)) => Ok(Event::Key(
                            crate::event::keyboard_types::from_crossterm(key),
                        )),
                        _ => Ok(Event::New(Cause::WaitOver)),
                    })
                    .forward(events_tx)
                    .await
                    .unwrap();
            }
        };

        spawn(Box::pin(task));

        Self {
            events_tx,
            events_rx,
            app,
        }
    }

    /// Run the event loop
    ///
    /// This will also initialize the terminal for rendering views. In
    /// particular, raw mode is enabled then an alternate screen is entered and
    /// then cleared. Note however that the location of the cursor is undefined.
    /// When the event loop is destroyed, the alternate screen is exited and raw
    /// mode is disabled.
    pub async fn run(mut self) {
        enable_raw_mode().unwrap();

        let mut stdout = std::io::stdout();

        execute!(stdout, EnterAlternateScreen, Clear(ClearType::All), Hide,)
            .unwrap();

        self.events_tx.send(Event::New(Cause::Init)).await.unwrap();

        let cmd_queues: Amv<Am<CommandBuf>> = Default::default();

        let new_cmd_queue = || {
            let cmd_queue: Am<CommandBuf> = Default::default();

            cmd_queues.lock().push(cmd_queue.clone());

            cmd_queue
        };

        let cmd_queue = new_cmd_queue();

        let mut printer =
            Printer::new(size().unwrap(), cmd_queue, &new_cmd_queue);

        while let Some(event) = self.events_rx.next().await {
            // The root view always has focus
            let messages = self.app.view().event(&event, true);

            for message in messages {
                self.app.update(message);
            }

            let control_flow = self.app.control_flow();

            if control_flow == ControlFlow::Exit {
                break;
            }

            let _resized = if let Event::Resize(new_size) = event {
                printer.set_size(new_size);

                true
            } else {
                false
            };

            let view = self.app.view();

            stdout.execute(Clear(ClearType::All)).unwrap();

            // The root view always has focus
            view.draw(&printer, true);

            let cmd_queues = cmd_queues.lock();

            let mut queue_cmds = |changes_cursor: bool| {
                for cmd_queue in cmd_queues
                    .iter()
                    .filter(|x| x.lock().changes_cursor == changes_cursor)
                {
                    let mut cmd_queue = cmd_queue.lock();

                    for cmd in cmd_queue.cmds.iter() {
                        stdout.queue(cmd).unwrap();
                    }

                    cmd_queue.cmds.clear();
                }
            };

            queue_cmds(false);

            // Make sure that command buffers (I mean, there should only be one)
            // that want to show the cursor at a certain location are queued
            // last, because otherwise the cursor will probably (it depends on
            // the contents of the screen) show in the wrong place.
            queue_cmds(true);

            stdout.flush().unwrap();
        }

        execute!(stdout, LeaveAlternateScreen, Show).unwrap();
        disable_raw_mode().unwrap();
    }
}