termit 0.7.0

Terminal UI over crossterm
Documentation
use crate::input::events::{AppEvent, Event};
use crate::input::{squash_input, Merge, Refresh};
use crate::output::command::{
    CaptureFocus, CaptureMouse, EnterRawMode, Flush, ResetStyles, ShowCursor, TerminalCommander,
    TerminalRequest, UseAlternateScreen, WrapLines,
};
use crate::output::io::CountingWrites;
use crate::sys::Control;
use crate::widget::Widget;
use crate::{geometry::*, Terminal};
use crate::{Painter, Screen};
use async_channel::Sender;
use futures_lite::prelude::*;
use log::*;
use std::io;
use std::io::BufWriter;
use std::pin::Pin;
use std::time::Duration;

/// The terminal app facilitator
///
/// Use [`crate::Terminal`] to set it up and initialize it.
///
/// The simplest approach is to call `step()` - that takes care of processing input.
///
/// If you're in a place without async, you can still make it turn
/// calling `update` and dealing with input differently, or block on the `step()`.
///
/// Finally, Termit exposes `input()` and `input_squashed()` you can use with `update()`
/// to do some inspection/transformation of the input.
pub struct Termit<'a, A: AppEvent> {
    control: Box<dyn Control + 'a>,
    input_closed: bool,
    events: EventStream<'a, A>,
    events_included: EventStream<'a, A>,
    refresh_time_sender: Sender<Duration>,
    out: Box<dyn io::Write + 'a>,
    screen: Screen,
    painter: Painter,
    run_without_input: bool,
}
type EventStream<'a, A> = Pin<Box<dyn Stream<Item = io::Result<Event<A>>> + Unpin + 'a>>;

impl<'a, A: AppEvent> Termit<'a, A> {
    /// Constructor called by [`crate::Terminal::into_termit()`]
    ///
    /// You probably don't want to call this unless you're into something special.
    pub fn new(
        control: impl Control + 'a,
        inp: impl AsyncRead + 'a,
        out: impl io::Write + 'a,
    ) -> Self {
        let raw_mode = control.is_raw();
        let input_control = control.clone();
        let events = Box::pin(crate::input::terminal_input_stream(
            input_control,
            inp,
            raw_mode,
        ));
        let refresh = Refresh::new(Duration::from_secs(1));
        let refresh_time_sender = refresh.time_sender();
        let events_included = Box::pin(refresh.map(io::Result::Ok));
        let run_without_input = false;
        let out_control = control.clone();
        let out = Box::new(BufWriter::with_capacity(
            1024 * 8,
            TerminalResetOnDrop(out_control, out),
        ));
        let control = Box::new(control);
        let size = control.get_size().unwrap_or_default();
        let painter = Painter::default().with_scope(size.window());
        let mut screen = Screen::new(size);
        screen.wipe = true;
        Self {
            control,
            events,
            events_included,
            out,
            screen,
            run_without_input,
            painter,
            refresh_time_sender,
            input_closed: false,
        }
    }

    /// Set this to true to draw over the previous screen content.
    /// This would be useful for instance if you're doing in-terminal clock or some fancy prompt
    /// But most apps probably want their own space, which is the default.
    /// Non-ttys should drawing over as there's nothing to draw over.
    pub fn draw_over(mut self, draw_over: bool) -> Self {
        self.screen.wipe = !draw_over;
        self
    }

    /// Set this to true if you know the terminal supports it for slightly less IO.
    pub fn true_color(mut self, true_color: bool) -> Self {
        self.screen.true_color = true_color;
        self
    }

    /// By default, termit generates an unexpectedd EOF when
    /// the input is closed. Set this to true
    /// to make it continue running even without input.
    /// This is useful for apps that do not take input.
    pub fn run_without_input(mut self, run: bool) -> Self {
        self.run_without_input = run;
        self
    }

    /// Step one cycle through the event loop, update the model and render the UI.
    ///
    /// Typically, you'll be calling this in a loop where you'll also
    /// be processing quick application logic (commands).
    ///
    /// It returns the number of input events processed.
    pub async fn step<M>(
        &mut self,
        model: &mut M,
        ui: &mut impl Widget<M, A>,
    ) -> io::Result<usize> {
        let mut count = 0;

        let bunch = squash_input(Merge::new(&mut self.events, &mut self.events_included))
            .next()
            .await;

        match bunch {
            Some(input) => {
                for input in input {
                    self.input_closed |= matches!(input, Ok(Event::InputClosed));

                    self.update(model, input?, ui);
                    count += 1;
                }
            }
            None => {
                return Err(io::Error::new(
                    io::ErrorKind::UnexpectedEof,
                    "input event stream is closed",
                ));
            }
        }

        if let Err(e) = self.print(None) {
            return Err(io::Error::new(e.kind(), format!("output error {e}")));
        }

        if !self.run_without_input && self.input_closed {
            return Err(io::Error::new(
                io::ErrorKind::UnexpectedEof,
                "input is closed",
            ));
        }

        Ok(count)
    }

    /// Allows you to update the refresh interval
    pub fn refresh_time_sender(&self) -> Sender<Duration> {
        self.refresh_time_sender.clone()
    }

    /// The screen buffer.
    /// Use this if you need to modify the screen directly
    pub fn screen_mut(&mut self) -> &mut Screen {
        &mut self.screen
    }
    /// The input event stream.
    /// Use this if you need to process events outside of UI
    pub fn input(&'a mut self) -> impl Stream<Item = io::Result<Event<A>>> + 'a {
        Merge::new(&mut self.events, &mut self.events_included)
    }
    pub fn replace_input<A2: AppEvent, F>(self, replacement: F) -> Termit<'a, A2>
    where
        F: Fn(EventStream<'a, A>) -> EventStream<'a, A2>,
    {
        let Self {
            control,
            events,
            events_included,
            out,
            screen,
            painter,
            run_without_input,
            refresh_time_sender,
            input_closed,
        } = self;
        let events = replacement(events);
        let events_included = replacement(events_included);
        Termit {
            control,
            events,
            events_included,
            out,
            screen,
            painter,
            run_without_input,
            refresh_time_sender,
            input_closed,
        }
    }
    pub fn include_input(
        mut self,
        additional: impl Stream<Item = io::Result<Event<A>>> + 'a,
    ) -> Self {
        self.events = Box::pin(Merge::new(self.events, additional));
        self
    }
    pub fn include_app_input(mut self, additional: impl Stream<Item = A> + 'a) -> Self {
        self.events = Box::pin(Merge::new(
            self.events,
            additional.map(|ev| Ok(Event::App(ev))),
        ));
        self
    }

    /// Print UI updates
    ///
    /// You can print only from a given scope, or pass None for whole screen
    ///
    /// This is called by the `step` method so you probably won't call this directly
    /// unless you need to handle output differently
    /// than the `step` method.
    pub fn print(&mut self, scope: Option<Window>) -> io::Result<()> {
        let mut out = CountingWrites::new(self.out.as_mut());
        let commander = (self.control.as_ref(), &mut out);
        self.screen.print(scope, commander)?;
        if out.count != 0 {
            trace!("IO: {}", out.count);
        }
        if out.busy {
            self.out.flush()?;
        }
        Ok(())
    }
    /// Handle input, update the model and widgets
    ///  
    /// This is called by the `step` method so you probably won't call this directly
    /// unless you need to pre-process the events or do something differently
    /// than the `step` method.
    pub fn update<M>(&mut self, model: &mut M, input: Event<A>, root: &mut impl Widget<M, A>) {
        if let Event::Resize(new_size) = input {
            // resize screen if necessary
            if self.screen.size() != new_size {
                self.screen.resize(new_size)
            }
            self.painter = self.painter.with_scope(new_size.window());
        }

        root.update_asserted(model, &input, &mut self.screen, &self.painter);
    }

    /// Most useful for TUIs, gives access to unfiltered/unbuffered input
    pub fn enter_raw_mode(mut self) -> io::Result<Self> {
        self.send(EnterRawMode)?;
        Ok(self)
    }
    /// Set this to true to draw the UI on an alternate screen.
    /// This helps not to overwrite previous console output.
    pub fn use_alternate_screen(mut self, alt_screen: bool) -> io::Result<Self> {
        self.send(UseAlternateScreen(alt_screen))?;
        Ok(self)
    }
    /// Set this to true to receive mouse input events
    pub fn capture_mouse(mut self, capture: bool) -> io::Result<Self> {
        self.send(CaptureMouse(capture))?;
        Ok(self)
    }
    /// Wrap around the end? Should not be useful in a well made TUI.
    /// You can set it to false.
    pub fn enable_line_wrap(mut self, enable: bool) -> io::Result<Self> {
        self.send(WrapLines(enable))?;
        Ok(self)
    }
    /// Show the cursor? Probably not in a TUI...
    pub fn show_cursor(mut self, show: bool) -> io::Result<Self> {
        self.send(ShowCursor(show))?;
        Ok(self)
    }
    /// Handle focus in and out? Can be handy.
    pub fn handle_focus_events(mut self, handle: bool) -> io::Result<Self> {
        self.send(CaptureFocus(handle))?;
        Ok(self)
    }

    // pub fn get_cursor_position(&mut self) -> Option<Point>
    // where
    //     C: io::Read,
    // {
    //     // TODO: replace with r/w on c,o
    //     cursor::position().ok().map(|(c, r)| point(c, r))
    // }
}

impl<'a> Termit<'a, NoAppEvent> {
    pub fn with_app_event_type<A: AppEvent>(self) -> Termit<'a, A> {
        self.replace_input(|stream| {
            Box::pin(stream.map(|e| {
                Ok(match e? {
                    Event::App(_) => panic!("No conversion between old and new app event"),
                    Event::Key(x) => Event::Key(x),
                    Event::Focus(f) => Event::Focus(f),
                    Event::CursorPosition(x) => Event::CursorPosition(x),
                    Event::Resize(x) => Event::Resize(x),
                    Event::Mouse(x) => Event::Mouse(x),
                    Event::Paste(x) => Event::Paste(x),
                    Event::Unknown(x) => Event::Unknown(x),
                    Event::Refresh(x) => Event::Refresh(x),
                    Event::InputClosed => Event::InputClosed,
                })
            }))
        })
    }
}

impl<'a, C, I, O, A: AppEvent> From<Terminal<C, I, O>> for Termit<'a, A>
where
    C: Control + 'a,
    I: AsyncRead + 'a,
    O: io::Write + 'a,
{
    fn from(terminal: Terminal<C, I, O>) -> Self {
        Termit::new(terminal.control, terminal.input, terminal.output)
    }
}

impl<A: AppEvent> TerminalCommander for Termit<'_, A> {
    fn send<T: crate::output::command::TerminalRequest>(&mut self, request: T) -> io::Result<()> {
        let mut commander = (self.control.as_ref(), self.out.as_mut());
        commander.send(request)
    }
}

/// No application event.
///
/// Cannot be constructed. Use this to signal that termit
/// doesn't handle application specific events.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NoAppEvent {}
impl Default for NoAppEvent {
    fn default() -> Self {
        unreachable!("cannot construct empty enum")
    }
}

struct TerminalResetOnDrop<C: Control, W: io::Write>(C, W);
impl<C: Control, W: io::Write> io::Write for TerminalResetOnDrop<C, W> {
    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
        W::write(&mut self.1, buf)
    }

    fn flush(&mut self) -> io::Result<()> {
        W::flush(&mut self.1)
    }
}
impl<C: Control, W: io::Write> TerminalCommander for TerminalResetOnDrop<C, W> {
    fn send<T: TerminalRequest>(&mut self, request: T) -> io::Result<()> {
        (&self.0, &mut self.1).send(request)
    }
}
impl<C: Control, W: io::Write> Drop for TerminalResetOnDrop<C, W> {
    fn drop(&mut self) {
        trace!("Resetting output");
        // match saved_position {
        //     Some((x, y)) => {
        //         out.execute(cursor::MoveTo(*x, *y))
        //             .expect("Restoring cursor position");
        //     }
        //     None => {
        //         if !*wiped {
        //             // dunno what to do, probably nothing
        //             warn!("Unclear situation about restoring cursor position");
        //         } else if let Some((_, height)) = terminal::size().ok() {
        //             out.execute(cursor::MoveToRow(height.saturating_sub(1)))
        //                 .expect("Reseting cursor");
        //             out.execute(style::Print("\n")).expect("Reseting last line");
        //         }
        //     }
        // };
        self.send(UseAlternateScreen(false))
            .expect("Reseting alt screen");
        self.send(ResetStyles).expect("Reseting style");
        self.send(CaptureMouse(false))
            .expect("Reseting mouse capture");
        self.send(ShowCursor(true)).expect("Reseting cursor");
        self.send(WrapLines(true)).expect("Reseting line wrap");
        self.send(CaptureFocus(false))
            .expect("Reseting focus changes");
        self.send(Flush).expect("Flushing reset");
    }
}