termit-ui 0.0.1

Terminal UI with GUI-like layouts
Documentation
use super::backoff;
use crate::command::*;
use crate::geometry::*;
use crate::input::*;
use crate::mion::color::*;
use crate::*;
use ::termion::input::TermRead;
use ::termion::raw::IntoRawMode;
use std::fmt::Display;
use std::io;
use std::io::{stdout, Stdout, Write};
use std::sync::mpsc::channel;
use std::sync::mpsc::SendError;
use std::sync::mpsc::{Receiver, RecvTimeoutError, Sender};
use std::time::Instant;

pub fn start_gui() -> io::Result<(
    TermionGUI,
    Receiver<io::Result<GrinInput>>,
    Sender<GrinCommand>,
)> {
    trace!("termion gui starting");
    let (command_tx, command) = channel();
    let (input, input_rx) = channel();
    if let Ok(mut stdout) = stdout().into_raw_mode() {
        let mut stdin = termion::async_stdin();
        let color_support = get_color_support(&mut stdin, &mut stdout)?;
        let terminal = termion::input::MouseTerminal::from(stdout);
        let ui = TermionGUI {
            command,
            input,
            size: Default::default(),
            terminal,
            events: stdin.events(),
            quitting: false,
            active: Instant::now(),
            restore: format!("{}", termion::cursor::Show),
            color_support: color_support,
            mouse_drag: false,
        };
        Ok((ui, input_rx, command_tx))
    } else {
        Err(io::Error::new(
            io::ErrorKind::Other,
            format!("expected a TTY!"),
        ))
    }
}

pub struct TermionGUI {
    size: Point,
    command: Receiver<GrinCommand>,
    input: Sender<Result<GrinInput, io::Error>>,
    terminal: termion::input::MouseTerminal<termion::raw::RawTerminal<Stdout>>,
    events: termion::input::Events<termion::AsyncReader>,
    color_support: ColorSupport,
    quitting: bool,
    active: Instant,
    restore: String,
    mouse_drag: bool,
}

impl GUI for TermionGUI {
    fn react(&mut self) -> io::Result<()> {
        self.react_once()
    }
}

impl TermionGUI {
    pub fn react_forever(&mut self) -> io::Result<()> {
        self.process(false)
    }

    pub fn react_once(&mut self) -> io::Result<()> {
        self.process(true)
    }

    fn process(&mut self, once: bool) -> io::Result<()> {
        while !self.quitting {
            while let Some(event) = self.events.next() {
                self.active = Instant::now();
                self.process_input(event)?;
            }
            self.process_commands()?;
            self.process_resize()?;
            if once {
                break;
            };
        }
        Ok(())
    }

    fn process_input(&mut self, event: Result<termion::event::Event, io::Error>) -> io::Result<()> {
        use termion::event::Event::*;
        let e = Ok(match event? {
            Key(key) => GrinInput::Key(key.into()),
            Mouse(m) => GrinInput::Mouse(m.into()),
            Unsupported(bytes) => GrinInput::Raw(bytes),
        });

        // fix missing drag
        let e = match e {
            Ok(GrinInput::Mouse(MouseEvent { position, action })) => match action {
                MouseAction::Release => {
                    self.mouse_drag = false;
                    e
                }
                MouseAction::PressLeft | MouseAction::PressMiddle | MouseAction::PressRight => {
                    if self.mouse_drag {
                        Ok(GrinInput::Mouse(MouseEvent {
                            position,
                            action: MouseAction::Drag,
                        }))
                    } else {
                        self.mouse_drag = true;
                        Ok(GrinInput::Mouse(MouseEvent { position, action }))
                    }
                }
                _ => e,
            },
            _ => e,
        };

        // send the next key input
        match self.input.send(e) {
            Ok(()) => Ok(()),
            Err(SendError(e)) => Err(io::Error::new(
                io::ErrorKind::Other,
                format!("Failed to send {:?}", e),
            )),
        }
    }

    fn process_commands(&mut self) -> io::Result<()> {
        loop {
            // process next event if any
            let idle = self.active.elapsed();
            let recv = self.command.recv_timeout(backoff(idle));

            if recv.is_ok() {
                self.active = Instant::now();
            }

            match recv {
                Err(RecvTimeoutError::Timeout) => break,
                Err(RecvTimeoutError::Disconnected) => {
                    self.exit("disconnect")?;
                    break;
                }
                Ok(GrinCommand::Quit) => {
                    self.exit("quit")?;
                    break;
                }
                Ok(GrinCommand::ShowCursor(show)) => match show {
                    false => self.write(termion::cursor::Hide)?,
                    true => self.write(termion::cursor::Show)?,
                },
                Ok(GrinCommand::Draw(d)) => {
                    if self.size.area() == 0 {
                        // don't bother with drawing on nothing
                        return Ok(());
                    }

                    let fg = termion_color(d.style.foreground, true, self.color_support);
                    let bg = termion_color(d.style.background, false, self.color_support);
                    let mut pos = d.scope.position;
                    self.write(format!("{}{}", bg, fg))?;
                    for l in d.lines() {
                        self.write(format!("{}", pos.goto()))?;
                        self.write(l)?;
                        pos = pos + y(1);
                    }
                    self.flush()?;
                }
            };
        }
        Ok(())
    }
    fn process_resize(&mut self) -> io::Result<()> {
        // handle resizing (size change)
        let size = termion::terminal_size()
            .map(|(tx, ty)| point(x(tx), y(ty)))
            .unwrap_or(Point::default());
        if size != self.size {
            self.size = size;
            self.input
                .send(Ok(GrinInput::Resize(size)))
                .expect("unable to send resize event");
        }
        Ok(())
    }

    fn exit(&mut self, info: impl Display) -> io::Result<()> {
        info!("ui exit: {}", info);
        self.quitting = true;
        let restore = format!("{}{}", point(x(0), self.size.y).goto(), self.restore);
        self.write(restore)?;
        self.flush()
    }

    fn write<T: Display>(&mut self, text: T) -> io::Result<()> {
        write!(self.terminal, "{}", text)
    }

    fn flush(&mut self) -> io::Result<()> {
        // Flush stdout (i.e. make the output appear).
        self.terminal.flush()
    }
}

impl Drop for TermionGUI {
    fn drop(&mut self) {
        self.exit("drop")
            .unwrap_or_else(|e| warn!("Failed to exit in drop() {}", e))
    }
}

impl From<termion::event::MouseEvent> for MouseEvent {
    fn from(m: termion::event::MouseEvent) -> MouseEvent {
        use termion::event::MouseButton::*;
        use termion::event::MouseEvent as T;
        match m {
            T::Press(Left, tx, ty) => MouseEvent {
                position: point(x(tx - 1), y(ty - 1)),
                action: MouseAction::PressLeft,
            },
            T::Press(Right, tx, ty) => MouseEvent {
                position: point(x(tx - 1), y(ty - 1)),
                action: MouseAction::PressRight,
            },
            T::Press(Middle, tx, ty) => MouseEvent {
                position: point(x(tx - 1), y(ty - 1)),
                action: MouseAction::PressMiddle,
            },
            T::Press(WheelUp, tx, ty) => MouseEvent {
                position: point(x(tx - 1), y(ty - 1)),
                action: MouseAction::WheelUp,
            },
            T::Press(WheelDown, tx, ty) => MouseEvent {
                position: point(x(tx - 1), y(ty - 1)),
                action: MouseAction::WheelDown,
            },
            T::Hold(tx, ty) => MouseEvent {
                position: point(x(tx - 1), y(ty - 1)),
                action: MouseAction::Drag,
            },
            T::Release(tx, ty) => MouseEvent {
                position: point(x(tx - 1), y(ty - 1)),
                action: MouseAction::Release,
            },
        }
    }
}

impl From<termion::event::Key> for KeyEvent {
    fn from(k: termion::event::Key) -> KeyEvent {
        use crate::input::Key as K;
        use termion::event::Key as T;
        let key = match k {
            T::Backspace => K::Backspace,
            T::Left => K::Left,
            T::Right => K::Right,
            T::Up => K::Up,
            T::Down => K::Down,
            T::Home => K::Home,
            T::End => K::End,
            T::PageUp => K::PageUp,
            T::PageDown => K::PageDown,
            T::Delete => K::Delete,
            T::Insert => K::Insert,
            T::F(f) => K::F(f),
            T::Char(c) => K::Char(c),
            T::Alt(c) => K::Char(c),
            T::Ctrl(c) => K::Char(c),
            T::Null => K::Null,
            T::Esc => K::Esc,
            T::__IsNotComplete => K::Other(0),
        };
        let ctrl = match k {
            T::Ctrl(_) => true,
            _ => false,
        };
        let alt = match k {
            T::Alt(_) => true,
            _ => false,
        };
        KeyEvent { key, ctrl, alt }
    }
}

trait IntoGoTo {
    fn goto(&self) -> termion::cursor::Goto;
}

impl IntoGoTo for Point {
    fn goto(&self) -> termion::cursor::Goto {
        termion::cursor::Goto(self.x.to_primitive() + 1, self.y.to_primitive() + 1)
    }
}