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),
});
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,
};
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 {
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 {
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<()> {
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<()> {
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)
}
}