2018-04-03 01:08:12 +00:00
|
|
|
//! Backend using the pure-rust termion library.
|
|
|
|
//!
|
|
|
|
//! Requires the `termion-backend` feature.
|
|
|
|
#![cfg(feature = "termion")]
|
|
|
|
|
2016-10-10 00:16:35 +00:00
|
|
|
extern crate termion;
|
|
|
|
|
2018-07-08 19:48:37 +00:00
|
|
|
use crossbeam_channel::{self, Sender, Receiver};
|
2016-12-13 23:05:35 +00:00
|
|
|
use self::termion::color as tcolor;
|
2017-03-25 18:01:25 +00:00
|
|
|
use self::termion::event::Event as TEvent;
|
2016-10-24 18:16:56 +00:00
|
|
|
use self::termion::event::Key as TKey;
|
2017-10-13 04:24:17 +00:00
|
|
|
use self::termion::event::MouseButton as TMouseButton;
|
|
|
|
use self::termion::event::MouseEvent as TMouseEvent;
|
|
|
|
use self::termion::input::{MouseTerminal, TermRead};
|
|
|
|
use self::termion::raw::{IntoRawMode, RawTerminal};
|
2017-04-26 06:20:52 +00:00
|
|
|
use self::termion::screen::AlternateScreen;
|
2016-12-13 23:05:35 +00:00
|
|
|
use self::termion::style as tstyle;
|
2018-07-08 19:48:37 +00:00
|
|
|
use signal_hook::iterator::Signals;
|
|
|
|
use libc;
|
|
|
|
|
2017-03-25 18:01:25 +00:00
|
|
|
use backend;
|
2017-10-13 04:24:17 +00:00
|
|
|
use event::{Event, Key, MouseButton, MouseEvent};
|
2018-07-08 19:48:37 +00:00
|
|
|
use theme;
|
|
|
|
use vec::Vec2;
|
|
|
|
|
2016-10-22 00:18:26 +00:00
|
|
|
use std::cell::Cell;
|
2017-10-13 04:24:17 +00:00
|
|
|
use std::io::{Stdout, Write};
|
2016-12-13 23:05:35 +00:00
|
|
|
use std::thread;
|
2018-07-08 19:48:37 +00:00
|
|
|
use std::sync::Arc;
|
|
|
|
use std::sync::atomic::{AtomicBool,Ordering};
|
2016-10-08 23:47:15 +00:00
|
|
|
|
2018-04-01 23:39:03 +00:00
|
|
|
pub struct Backend {
|
2017-10-13 04:24:17 +00:00
|
|
|
terminal: AlternateScreen<MouseTerminal<RawTerminal<Stdout>>>,
|
2017-06-13 01:31:08 +00:00
|
|
|
current_style: Cell<theme::ColorPair>,
|
2018-05-20 16:59:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
struct InputParser {
|
2018-07-08 19:48:37 +00:00
|
|
|
// Inner state required to parse input
|
2017-10-13 04:24:17 +00:00
|
|
|
last_button: Option<MouseButton>,
|
2018-07-08 19:48:37 +00:00
|
|
|
|
|
|
|
event_due: bool,
|
|
|
|
requests: Sender<()>,
|
|
|
|
input: Receiver<TEvent>,
|
2016-10-22 00:18:26 +00:00
|
|
|
}
|
|
|
|
|
2018-05-20 16:59:35 +00:00
|
|
|
impl InputParser {
|
2018-07-08 19:48:37 +00:00
|
|
|
// Creates a non-blocking abstraction over the usual blocking input
|
|
|
|
fn new() -> Self {
|
|
|
|
let (input_sender, input_receiver) = crossbeam_channel::bounded(0);
|
|
|
|
let (request_sender, request_receiver) = crossbeam_channel::bounded(0);
|
2018-06-18 00:26:03 +00:00
|
|
|
|
2018-07-08 19:48:37 +00:00
|
|
|
// This thread will stop after an event when `InputParser` is dropped.
|
2018-04-01 23:39:03 +00:00
|
|
|
thread::spawn(move || {
|
2018-07-08 19:48:37 +00:00
|
|
|
let stdin = ::std::io::stdin();
|
|
|
|
let stdin = stdin.lock();
|
|
|
|
let mut events = stdin.events();
|
|
|
|
|
|
|
|
for _ in request_receiver {
|
|
|
|
let event: Result<TEvent, ::std::io::Error> = events.next().unwrap();
|
|
|
|
input_sender.send(event.unwrap());
|
2018-04-01 23:39:03 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2018-05-20 16:59:35 +00:00
|
|
|
InputParser {
|
2018-04-01 23:39:03 +00:00
|
|
|
last_button: None,
|
2018-07-08 19:48:37 +00:00
|
|
|
input: input_receiver,
|
|
|
|
requests: request_sender,
|
|
|
|
event_due: false,
|
2018-05-20 16:59:35 +00:00
|
|
|
}
|
|
|
|
}
|
2018-04-01 23:39:03 +00:00
|
|
|
|
2018-07-08 19:48:37 +00:00
|
|
|
/// We pledge to want input.
|
|
|
|
///
|
|
|
|
/// If we were already expecting input, this is a NO-OP.
|
|
|
|
fn request(&mut self) {
|
|
|
|
if !self.event_due {
|
|
|
|
self.requests.send(());
|
|
|
|
self.event_due = true;
|
2018-05-20 16:59:35 +00:00
|
|
|
}
|
2018-07-08 19:48:37 +00:00
|
|
|
}
|
2018-05-20 16:59:35 +00:00
|
|
|
|
2018-07-08 19:48:37 +00:00
|
|
|
fn peek(&mut self) -> Option<Event> {
|
|
|
|
self.request();
|
|
|
|
|
|
|
|
let timeout = ::std::time::Duration::from_millis(10);
|
|
|
|
|
|
|
|
let input = select! {
|
|
|
|
recv(self.input, input) => {
|
|
|
|
input
|
|
|
|
}
|
|
|
|
recv(crossbeam_channel::after(timeout)) => return None,
|
|
|
|
};
|
|
|
|
|
|
|
|
// We got what we came for.
|
|
|
|
self.event_due = false;
|
|
|
|
Some(self.map_key(input.unwrap()))
|
2018-04-01 23:39:03 +00:00
|
|
|
}
|
|
|
|
|
2018-07-08 19:48:37 +00:00
|
|
|
fn next_event(&mut self) -> Event {
|
|
|
|
self.request();
|
|
|
|
|
|
|
|
let input = self.input.recv().unwrap();
|
|
|
|
self.event_due = false;
|
|
|
|
self.map_key(input)
|
2016-10-22 00:18:26 +00:00
|
|
|
}
|
2018-05-20 16:59:35 +00:00
|
|
|
|
2017-10-13 04:24:17 +00:00
|
|
|
fn map_key(&mut self, event: TEvent) -> Event {
|
|
|
|
match event {
|
|
|
|
TEvent::Unsupported(bytes) => Event::Unknown(bytes),
|
|
|
|
TEvent::Key(TKey::Esc) => Event::Key(Key::Esc),
|
|
|
|
TEvent::Key(TKey::Backspace) => Event::Key(Key::Backspace),
|
|
|
|
TEvent::Key(TKey::Left) => Event::Key(Key::Left),
|
|
|
|
TEvent::Key(TKey::Right) => Event::Key(Key::Right),
|
|
|
|
TEvent::Key(TKey::Up) => Event::Key(Key::Up),
|
|
|
|
TEvent::Key(TKey::Down) => Event::Key(Key::Down),
|
|
|
|
TEvent::Key(TKey::Home) => Event::Key(Key::Home),
|
|
|
|
TEvent::Key(TKey::End) => Event::Key(Key::End),
|
|
|
|
TEvent::Key(TKey::PageUp) => Event::Key(Key::PageUp),
|
|
|
|
TEvent::Key(TKey::PageDown) => Event::Key(Key::PageDown),
|
|
|
|
TEvent::Key(TKey::Delete) => Event::Key(Key::Del),
|
|
|
|
TEvent::Key(TKey::Insert) => Event::Key(Key::Ins),
|
|
|
|
TEvent::Key(TKey::F(i)) if i < 12 => Event::Key(Key::from_f(i)),
|
|
|
|
TEvent::Key(TKey::F(j)) => Event::Unknown(vec![j]),
|
|
|
|
TEvent::Key(TKey::Char('\n')) => Event::Key(Key::Enter),
|
|
|
|
TEvent::Key(TKey::Char('\t')) => Event::Key(Key::Tab),
|
|
|
|
TEvent::Key(TKey::Char(c)) => Event::Char(c),
|
|
|
|
TEvent::Key(TKey::Ctrl('c')) => Event::Exit,
|
|
|
|
TEvent::Key(TKey::Ctrl(c)) => Event::CtrlChar(c),
|
|
|
|
TEvent::Key(TKey::Alt(c)) => Event::AltChar(c),
|
|
|
|
TEvent::Mouse(TMouseEvent::Press(btn, x, y)) => {
|
|
|
|
let position = (x - 1, y - 1).into();
|
|
|
|
|
|
|
|
let event = match btn {
|
|
|
|
TMouseButton::Left => MouseEvent::Press(MouseButton::Left),
|
|
|
|
TMouseButton::Middle => {
|
|
|
|
MouseEvent::Press(MouseButton::Middle)
|
|
|
|
}
|
|
|
|
TMouseButton::Right => {
|
|
|
|
MouseEvent::Press(MouseButton::Right)
|
|
|
|
}
|
|
|
|
TMouseButton::WheelUp => MouseEvent::WheelUp,
|
|
|
|
TMouseButton::WheelDown => MouseEvent::WheelDown,
|
|
|
|
};
|
|
|
|
|
|
|
|
if let MouseEvent::Press(btn) = event {
|
|
|
|
self.last_button = Some(btn);
|
|
|
|
}
|
|
|
|
|
|
|
|
Event::Mouse {
|
|
|
|
event,
|
|
|
|
position,
|
|
|
|
offset: Vec2::zero(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
TEvent::Mouse(TMouseEvent::Release(x, y))
|
|
|
|
if self.last_button.is_some() =>
|
|
|
|
{
|
|
|
|
let event = MouseEvent::Release(self.last_button.unwrap());
|
|
|
|
let position = (x - 1, y - 1).into();
|
|
|
|
Event::Mouse {
|
|
|
|
event,
|
|
|
|
position,
|
|
|
|
offset: Vec2::zero(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
TEvent::Mouse(TMouseEvent::Hold(x, y))
|
|
|
|
if self.last_button.is_some() =>
|
|
|
|
{
|
|
|
|
let event = MouseEvent::Hold(self.last_button.unwrap());
|
|
|
|
let position = (x - 1, y - 1).into();
|
|
|
|
Event::Mouse {
|
|
|
|
event,
|
|
|
|
position,
|
|
|
|
offset: Vec2::zero(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
_ => Event::Unknown(vec![]),
|
|
|
|
}
|
|
|
|
}
|
2016-10-08 23:47:15 +00:00
|
|
|
}
|
|
|
|
|
2018-05-20 16:59:35 +00:00
|
|
|
trait Effectable {
|
|
|
|
fn on(&self);
|
|
|
|
fn off(&self);
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Effectable for theme::Effect {
|
|
|
|
fn on(&self) {
|
|
|
|
match *self {
|
|
|
|
theme::Effect::Simple => (),
|
|
|
|
theme::Effect::Reverse => print!("{}", tstyle::Invert),
|
|
|
|
theme::Effect::Bold => print!("{}", tstyle::Bold),
|
|
|
|
theme::Effect::Italic => print!("{}", tstyle::Italic),
|
|
|
|
theme::Effect::Underline => print!("{}", tstyle::Underline),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn off(&self) {
|
|
|
|
match *self {
|
|
|
|
theme::Effect::Simple => (),
|
|
|
|
theme::Effect::Reverse => print!("{}", tstyle::NoInvert),
|
|
|
|
theme::Effect::Bold => print!("{}", tstyle::NoBold),
|
|
|
|
theme::Effect::Italic => print!("{}", tstyle::NoItalic),
|
|
|
|
theme::Effect::Underline => print!("{}", tstyle::NoUnderline),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Backend {
|
2018-06-18 00:26:03 +00:00
|
|
|
pub fn init() -> Box<backend::Backend> {
|
2018-05-20 16:59:35 +00:00
|
|
|
print!("{}", termion::cursor::Hide);
|
|
|
|
|
|
|
|
// TODO: lock stdout
|
|
|
|
let terminal = AlternateScreen::from(MouseTerminal::from(
|
|
|
|
::std::io::stdout().into_raw_mode().unwrap(),
|
|
|
|
));
|
|
|
|
|
|
|
|
let c = Backend {
|
|
|
|
terminal: terminal,
|
|
|
|
current_style: Cell::new(theme::ColorPair::from_256colors(0, 0)),
|
|
|
|
};
|
|
|
|
|
|
|
|
Box::new(c)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn apply_colors(&self, colors: theme::ColorPair) {
|
|
|
|
with_color(&colors.front, |c| print!("{}", tcolor::Fg(c)));
|
|
|
|
with_color(&colors.back, |c| print!("{}", tcolor::Bg(c)));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-04-01 23:39:03 +00:00
|
|
|
impl backend::Backend for Backend {
|
2016-10-08 23:47:15 +00:00
|
|
|
fn finish(&mut self) {
|
|
|
|
print!("{}{}", termion::cursor::Show, termion::cursor::Goto(1, 1));
|
2017-10-12 23:38:55 +00:00
|
|
|
print!(
|
|
|
|
"{}[49m{}[39m{}",
|
|
|
|
27 as char,
|
|
|
|
27 as char,
|
|
|
|
termion::clear::All
|
|
|
|
);
|
2016-10-08 23:47:15 +00:00
|
|
|
}
|
|
|
|
|
2018-04-01 22:35:37 +00:00
|
|
|
fn set_color(&self, color: theme::ColorPair) -> theme::ColorPair {
|
2016-10-22 00:18:26 +00:00
|
|
|
let current_style = self.current_style.get();
|
|
|
|
|
2017-06-13 06:51:41 +00:00
|
|
|
if current_style != color {
|
|
|
|
self.apply_colors(color);
|
|
|
|
self.current_style.set(color);
|
|
|
|
}
|
2016-10-22 00:18:26 +00:00
|
|
|
|
2018-04-01 22:35:37 +00:00
|
|
|
return current_style;
|
2016-10-08 23:47:15 +00:00
|
|
|
}
|
|
|
|
|
2018-04-01 22:35:37 +00:00
|
|
|
fn set_effect(&self, effect: theme::Effect) {
|
2016-10-24 18:16:56 +00:00
|
|
|
effect.on();
|
2018-04-01 22:35:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
fn unset_effect(&self, effect: theme::Effect) {
|
2016-10-24 18:16:56 +00:00
|
|
|
effect.off();
|
2016-10-08 23:47:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
fn has_colors(&self) -> bool {
|
|
|
|
// TODO: color support detection?
|
|
|
|
true
|
|
|
|
}
|
|
|
|
|
2018-04-03 01:08:12 +00:00
|
|
|
fn screen_size(&self) -> Vec2 {
|
2016-10-08 23:47:15 +00:00
|
|
|
let (x, y) = termion::terminal_size().unwrap_or((1, 1));
|
2018-04-03 01:08:12 +00:00
|
|
|
(x, y).into()
|
2016-10-08 23:47:15 +00:00
|
|
|
}
|
|
|
|
|
2017-06-13 06:29:26 +00:00
|
|
|
fn clear(&self, color: theme::Color) {
|
2017-06-13 06:51:41 +00:00
|
|
|
self.apply_colors(theme::ColorPair {
|
2017-06-13 06:29:26 +00:00
|
|
|
front: color,
|
|
|
|
back: color,
|
|
|
|
});
|
2016-10-08 23:47:15 +00:00
|
|
|
print!("{}", termion::clear::All);
|
|
|
|
}
|
|
|
|
|
|
|
|
fn refresh(&mut self) {
|
|
|
|
self.terminal.flush().unwrap();
|
|
|
|
}
|
|
|
|
|
2018-04-03 01:08:12 +00:00
|
|
|
fn print_at(&self, pos: Vec2, text: &str) {
|
2017-10-12 23:38:55 +00:00
|
|
|
print!(
|
|
|
|
"{}{}",
|
2018-04-03 01:08:12 +00:00
|
|
|
termion::cursor::Goto(1 + pos.x as u16, 1 + pos.y as u16),
|
2017-10-12 23:38:55 +00:00
|
|
|
text
|
|
|
|
);
|
2016-10-08 23:47:15 +00:00
|
|
|
}
|
|
|
|
|
2018-06-16 20:23:09 +00:00
|
|
|
fn start_input_thread(
|
2018-07-08 19:48:37 +00:00
|
|
|
&mut self, event_sink: Sender<Option<Event>>,
|
|
|
|
input_request: Receiver<backend::InputRequest>,
|
2018-06-16 20:23:09 +00:00
|
|
|
) {
|
2018-07-08 19:48:37 +00:00
|
|
|
let running = Arc::new(AtomicBool::new(true));
|
|
|
|
let resize_sender = event_sink.clone();
|
|
|
|
let signals = Signals::new(&[libc::SIGWINCH]).unwrap();
|
|
|
|
|
|
|
|
let resize_running = Arc::clone(&running);
|
2018-05-20 16:59:35 +00:00
|
|
|
thread::spawn(move || {
|
2018-07-08 19:48:37 +00:00
|
|
|
while resize_running.load(Ordering::Relaxed) {
|
|
|
|
// We know it will only contain SIGWINCH signals, so no need to check.
|
|
|
|
for _ in signals.pending() {
|
|
|
|
// Tell ncurses about the new terminal size.
|
|
|
|
// Well, do the actual resizing later on, in the main thread.
|
|
|
|
// Ncurses isn't really thread-safe so calling resize_term() can crash
|
|
|
|
// other calls like clear() or refresh().
|
|
|
|
resize_sender.send(Some(Event::WindowResize));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
2018-06-09 02:48:01 +00:00
|
|
|
|
2018-07-08 19:48:37 +00:00
|
|
|
let mut parser = InputParser::new();
|
|
|
|
thread::spawn(move || {
|
|
|
|
for req in input_request {
|
|
|
|
match req {
|
|
|
|
backend::InputRequest::Peek => {
|
|
|
|
event_sink.send(parser.peek());
|
|
|
|
}
|
|
|
|
backend::InputRequest::Block => {
|
|
|
|
event_sink.send(Some(parser.next_event()));
|
|
|
|
}
|
2018-06-09 02:48:01 +00:00
|
|
|
}
|
2016-12-14 06:10:00 +00:00
|
|
|
}
|
2018-07-08 19:48:37 +00:00
|
|
|
running.store(false, Ordering::Relaxed);
|
2018-05-20 16:59:35 +00:00
|
|
|
});
|
2016-12-13 23:05:35 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-06-13 01:31:08 +00:00
|
|
|
fn with_color<F, R>(clr: &theme::Color, f: F) -> R
|
2017-10-12 23:38:55 +00:00
|
|
|
where
|
|
|
|
F: FnOnce(&tcolor::Color) -> R,
|
2017-06-13 01:31:08 +00:00
|
|
|
{
|
|
|
|
match *clr {
|
2017-10-12 23:38:55 +00:00
|
|
|
theme::Color::TerminalDefault => f(&tcolor::Reset),
|
|
|
|
theme::Color::Dark(theme::BaseColor::Black) => f(&tcolor::Black),
|
|
|
|
theme::Color::Dark(theme::BaseColor::Red) => f(&tcolor::Red),
|
|
|
|
theme::Color::Dark(theme::BaseColor::Green) => f(&tcolor::Green),
|
|
|
|
theme::Color::Dark(theme::BaseColor::Yellow) => f(&tcolor::Yellow),
|
|
|
|
theme::Color::Dark(theme::BaseColor::Blue) => f(&tcolor::Blue),
|
|
|
|
theme::Color::Dark(theme::BaseColor::Magenta) => f(&tcolor::Magenta),
|
|
|
|
theme::Color::Dark(theme::BaseColor::Cyan) => f(&tcolor::Cyan),
|
|
|
|
theme::Color::Dark(theme::BaseColor::White) => f(&tcolor::White),
|
|
|
|
|
|
|
|
theme::Color::Light(theme::BaseColor::Black) => f(&tcolor::LightBlack),
|
|
|
|
theme::Color::Light(theme::BaseColor::Red) => f(&tcolor::LightRed),
|
|
|
|
theme::Color::Light(theme::BaseColor::Green) => f(&tcolor::LightGreen),
|
|
|
|
theme::Color::Light(theme::BaseColor::Yellow) => {
|
|
|
|
f(&tcolor::LightYellow)
|
|
|
|
}
|
|
|
|
theme::Color::Light(theme::BaseColor::Blue) => f(&tcolor::LightBlue),
|
|
|
|
theme::Color::Light(theme::BaseColor::Magenta) => {
|
|
|
|
f(&tcolor::LightMagenta)
|
|
|
|
}
|
|
|
|
theme::Color::Light(theme::BaseColor::Cyan) => f(&tcolor::LightCyan),
|
|
|
|
theme::Color::Light(theme::BaseColor::White) => f(&tcolor::LightWhite),
|
|
|
|
|
|
|
|
theme::Color::Rgb(r, g, b) => f(&tcolor::Rgb(r, g, b)),
|
|
|
|
theme::Color::RgbLowRes(r, g, b) => {
|
|
|
|
f(&tcolor::AnsiValue::rgb(r, g, b))
|
|
|
|
}
|
|
|
|
}
|
2016-10-22 00:18:26 +00:00
|
|
|
}
|