Crossterm 0.14 (#406)

Refactor and update crossterm backend.
This commit is contained in:
Timon 2020-01-02 09:08:53 +01:00 committed by Alexandre Bury
parent 8a018dbe4d
commit 54de6ca6c0
3 changed files with 273 additions and 179 deletions

View File

@ -74,7 +74,7 @@ version = "1.5.3"
[dependencies.crossterm] [dependencies.crossterm]
optional = true optional = true
version = "0.12" version = "0.14"
[target.'cfg(unix)'.dependencies] [target.'cfg(unix)'.dependencies]
signal-hook = "0.1.10" signal-hook = "0.1.10"

View File

@ -2,4 +2,3 @@ unstable_features = true
max_width = 79 max_width = 79
reorder_imports = true reorder_imports = true
fn_args_layout = "Tall" fn_args_layout = "Tall"

View File

@ -4,28 +4,198 @@
#![cfg(feature = "crossterm")] #![cfg(feature = "crossterm")]
use crate::vec::Vec2; use std::{
use crate::{backend, theme}; cell::{Cell, RefCell, RefMut},
use crossterm::{ fs::File,
cursor, input, queue, terminal, AlternateScreen, AsyncReader, Attribute, io::{self, BufWriter, Stdout, Write},
Clear, ClearType, Color, Goto, InputEvent as CInputEvent, time::Duration,
KeyEvent as CKeyEvent, MouseButton as CMouseButton,
MouseEvent as CMouseEvent, SetAttr, SetBg, SetFg, Terminal,
}; };
use crate::event::{Event, Key, MouseButton, MouseEvent}; use crossterm::{
use std::cell::{Cell, RefCell}; cursor::{Hide, MoveTo, Show},
use std::io::{self, BufWriter, Stdout, Write}; event::{
poll, read, DisableMouseCapture, EnableMouseCapture, Event as CEvent,
KeyCode, KeyEvent as CKeyEvent, KeyModifiers,
MouseButton as CMouseButton, MouseEvent as CMouseEvent,
},
execute, queue,
style::{
Attribute, Color, Print, SetAttribute, SetBackgroundColor,
SetForegroundColor,
},
terminal::{
self, disable_raw_mode, enable_raw_mode, Clear, ClearType,
EnterAlternateScreen, LeaveAlternateScreen,
},
};
use crate::{
backend,
event::{Event, Key, MouseButton, MouseEvent},
theme,
vec::Vec2,
};
/// Backend using crossterm /// Backend using crossterm
pub struct Backend { pub struct Backend {
current_style: Cell<theme::ColorPair>, current_style: Cell<theme::ColorPair>,
last_button: Option<MouseButton>, last_button: Option<MouseButton>,
// reader to read user input async.
async_reader: AsyncReader, #[cfg(windows)]
_alternate_screen: AlternateScreen,
stdout: RefCell<BufWriter<Stdout>>, stdout: RefCell<BufWriter<Stdout>>,
terminal: Terminal, #[cfg(unix)]
stdout: RefCell<BufWriter<File>>,
}
impl From<CMouseButton> for MouseButton {
fn from(button: CMouseButton) -> Self {
match button {
CMouseButton::Left => MouseButton::Left,
CMouseButton::Right => MouseButton::Right,
CMouseButton::Middle => MouseButton::Middle,
}
}
}
impl From<KeyCode> for Key {
fn from(code: KeyCode) -> Self {
match code {
KeyCode::Esc => Key::Esc,
KeyCode::Backspace => Key::Backspace,
KeyCode::Left => Key::Left,
KeyCode::Right => Key::Right,
KeyCode::Up => Key::Up,
KeyCode::Down => Key::Down,
KeyCode::Home => Key::Home,
KeyCode::End => Key::End,
KeyCode::PageUp => Key::PageUp,
KeyCode::PageDown => Key::PageDown,
KeyCode::Delete => Key::Del,
KeyCode::Insert => Key::Ins,
KeyCode::Enter => Key::Enter,
KeyCode::Tab => Key::Tab,
KeyCode::F(n) => Key::from_f(n),
KeyCode::BackTab => Key::Tab, /* not supported */
KeyCode::Char(_) => Key::Tab, /* is handled at `Event` level, use tab as default */
KeyCode::Null => Key::Tab, /* is handled at `Event` level, use tab as default */
}
}
}
impl From<CKeyEvent> for Event {
fn from(event: CKeyEvent) -> Self {
const CTRL_ALT: KeyModifiers = KeyModifiers::from_bits_truncate(
KeyModifiers::CONTROL.bits() | KeyModifiers::ALT.bits(),
);
const CTRL_SHIFT: KeyModifiers = KeyModifiers::from_bits_truncate(
KeyModifiers::CONTROL.bits() | KeyModifiers::SHIFT.bits(),
);
const ALT_SHIFT: KeyModifiers = KeyModifiers::from_bits_truncate(
KeyModifiers::ALT.bits() | KeyModifiers::SHIFT.bits(),
);
match event {
// Handle Char + modifier.
CKeyEvent {
modifiers: KeyModifiers::CONTROL,
code: KeyCode::Char('c'),
} => Event::Exit,
CKeyEvent {
modifiers: KeyModifiers::CONTROL,
code: KeyCode::Char(c),
} => Event::CtrlChar(c),
CKeyEvent {
modifiers: KeyModifiers::ALT,
code: KeyCode::Char(c),
} => Event::AltChar(c),
CKeyEvent {
modifiers: KeyModifiers::SHIFT,
code: KeyCode::Char(c),
} => Event::Char(c),
// Handle key + multiple modifiers
CKeyEvent {
modifiers: CTRL_ALT,
code,
} => Event::CtrlAlt(Key::from(code)),
CKeyEvent {
modifiers: CTRL_SHIFT,
code,
} => Event::CtrlShift(Key::from(code)),
CKeyEvent {
modifiers: ALT_SHIFT,
code,
} => Event::AltShift(Key::from(code)),
// Handle key + single modifier
CKeyEvent {
modifiers: KeyModifiers::CONTROL,
code,
} => Event::Ctrl(Key::from(code)),
CKeyEvent {
modifiers: KeyModifiers::ALT,
code,
} => Event::Alt(Key::from(code)),
CKeyEvent {
modifiers: KeyModifiers::SHIFT,
code,
} => Event::Shift(Key::from(code)),
CKeyEvent {
code: KeyCode::Char(c),
..
} => Event::Char(c),
// Explicitly handle 'backtab' since crossterm does not sent SHIFT alongside the back tab key.
CKeyEvent {
code: KeyCode::BackTab,
..
} => Event::Shift(Key::Tab),
// All other keys.
CKeyEvent { code, .. } => Event::Key(Key::from(code)),
}
}
}
impl From<theme::Color> for Color {
fn from(base_color: theme::Color) -> Self {
match base_color {
theme::Color::Dark(theme::BaseColor::Black) => Color::Black,
theme::Color::Dark(theme::BaseColor::Red) => Color::DarkRed,
theme::Color::Dark(theme::BaseColor::Green) => Color::DarkGreen,
theme::Color::Dark(theme::BaseColor::Yellow) => Color::DarkYellow,
theme::Color::Dark(theme::BaseColor::Blue) => Color::DarkBlue,
theme::Color::Dark(theme::BaseColor::Magenta) => {
Color::DarkMagenta
}
theme::Color::Dark(theme::BaseColor::Cyan) => Color::DarkCyan,
theme::Color::Dark(theme::BaseColor::White) => Color::Grey,
theme::Color::Light(theme::BaseColor::Black) => Color::DarkGrey,
theme::Color::Light(theme::BaseColor::Red) => Color::Red,
theme::Color::Light(theme::BaseColor::Green) => Color::Green,
theme::Color::Light(theme::BaseColor::Yellow) => Color::Yellow,
theme::Color::Light(theme::BaseColor::Blue) => Color::Blue,
theme::Color::Light(theme::BaseColor::Magenta) => Color::Magenta,
theme::Color::Light(theme::BaseColor::Cyan) => Color::Cyan,
theme::Color::Light(theme::BaseColor::White) => Color::White,
theme::Color::Rgb(r, g, b) => Color::Rgb { r, g, b },
theme::Color::RgbLowRes(r, g, b) => {
debug_assert!(r <= 5,
"Red color fragment (r = {}) is out of bound. Make sure r ≤ 5.",
r);
debug_assert!(g <= 5,
"Green color fragment (g = {}) is out of bound. Make sure g ≤ 5.",
g);
debug_assert!(b <= 5,
"Blue color fragment (b = {}) is out of bound. Make sure b ≤ 5.",
b);
Color::AnsiValue(16 + 36 * r + 6 * g + b)
}
theme::Color::TerminalDefault => Color::Reset,
}
}
} }
impl Backend { impl Backend {
@ -34,151 +204,119 @@ impl Backend {
where where
Self: Sized, Self: Sized,
{ {
let _alternate_screen = AlternateScreen::to_alternate(true)?; enable_raw_mode()?;
let input = input(); execute!(
let async_reader = input.read_async(); io::stdout(),
input.enable_mouse_mode().unwrap(); EnterAlternateScreen,
EnableMouseCapture,
Hide
)?;
cursor().hide()?; #[cfg(unix)]
let stdout = RefCell::new(BufWriter::new(File::create("/dev/tty")?));
#[cfg(windows)]
let stdout = RefCell::new(BufWriter::new(io::stdout()));
Ok(Box::new(Backend { Ok(Box::new(Backend {
current_style: Cell::new(theme::ColorPair::from_256colors(0, 0)), current_style: Cell::new(theme::ColorPair::from_256colors(0, 0)),
last_button: None, last_button: None,
async_reader, stdout,
_alternate_screen,
stdout: RefCell::new(BufWriter::new(io::stdout())),
terminal: terminal(),
})) }))
} }
fn apply_colors(&self, colors: theme::ColorPair) { fn apply_colors(&self, colors: theme::ColorPair) {
with_color(colors.front, |c| { queue!(
queue!(self.stdout.borrow_mut(), SetFg(*c)) self.stdout_mut(),
}) SetForegroundColor(Color::from(colors.front)),
.unwrap(); SetBackgroundColor(Color::from(colors.back))
with_color(colors.back, |c| { )
queue!(self.stdout.borrow_mut(), SetBg(*c))
})
.unwrap(); .unwrap();
} }
fn write<T>(&self, content: T) #[cfg(unix)]
where fn stdout_mut(&self) -> RefMut<BufWriter<File>> {
T: std::fmt::Display, self.stdout.borrow_mut()
{ }
write!(self.stdout.borrow_mut(), "{}", format!("{}", content))
.unwrap(); #[cfg(windows)]
fn stdout_mut(&self) -> RefMut<BufWriter<Stdout>> {
self.stdout.borrow_mut()
} }
fn set_attr(&self, attr: Attribute) { fn set_attr(&self, attr: Attribute) {
queue!(self.stdout.borrow_mut(), SetAttr(attr)).unwrap(); queue!(self.stdout_mut(), SetAttribute(attr)).unwrap();
} }
fn map_key(&mut self, event: CInputEvent) -> Event { fn map_key(&mut self, event: CEvent) -> Event {
match event { match event {
CInputEvent::Keyboard(key_event) => match key_event { CEvent::Key(key_event) => Event::from(key_event),
CKeyEvent::Esc => Event::Key(Key::Esc), CEvent::Mouse(mouse_event) => {
CKeyEvent::Backspace => Event::Key(Key::Backspace), let position;
CKeyEvent::Left => Event::Key(Key::Left), let event;
CKeyEvent::Right => Event::Key(Key::Right),
CKeyEvent::Up => Event::Key(Key::Up),
CKeyEvent::Down => Event::Key(Key::Down),
CKeyEvent::Home => Event::Key(Key::Home),
CKeyEvent::End => Event::Key(Key::End),
CKeyEvent::PageUp => Event::Key(Key::PageUp),
CKeyEvent::PageDown => Event::Key(Key::PageDown),
CKeyEvent::Delete => Event::Key(Key::Del),
CKeyEvent::Insert => Event::Key(Key::Ins),
CKeyEvent::Enter => Event::Key(Key::Enter),
CKeyEvent::Tab => Event::Key(Key::Tab),
CKeyEvent::F(n) => Event::Key(Key::from_f(n)),
CKeyEvent::Char(c) => Event::Char(c),
CKeyEvent::Ctrl('c') => Event::Exit,
CKeyEvent::Ctrl(c) => Event::CtrlChar(c),
CKeyEvent::Alt(c) => Event::AltChar(c),
_ => Event::Unknown(vec![]),
},
CInputEvent::Mouse(mouse_event) => match mouse_event {
CMouseEvent::Press(btn, x, y) => {
let position = (x, y).into();
let event = match btn { match mouse_event {
CMouseButton::Left => { CMouseEvent::Down(button, x, y, _) => {
MouseEvent::Press(MouseButton::Left) let button = MouseButton::from(button);
} self.last_button = Some(button);
CMouseButton::Middle => { event = MouseEvent::Press(button);
MouseEvent::Press(MouseButton::Middle) position = (x, y).into();
}
CMouseButton::Right => {
MouseEvent::Press(MouseButton::Right)
}
CMouseButton::WheelUp => MouseEvent::WheelUp,
CMouseButton::WheelDown => MouseEvent::WheelDown,
};
if let MouseEvent::Press(btn) = event {
self.last_button = Some(btn);
} }
CMouseEvent::Up(_, x, y, _) => {
Event::Mouse { event = MouseEvent::Release(self.last_button.unwrap());
event, position = (x, y).into();
position,
offset: Vec2::zero(),
} }
} CMouseEvent::Drag(_, x, y, _) => {
CMouseEvent::Release(x, y) if self.last_button.is_some() => { event = MouseEvent::Hold(self.last_button.unwrap());
let event = MouseEvent::Release(self.last_button.unwrap()); position = (x, y).into();
let position = (x, y).into();
Event::Mouse {
event,
position,
offset: Vec2::zero(),
} }
} CMouseEvent::ScrollDown(x, y, _) => {
CMouseEvent::Hold(x, y) if self.last_button.is_some() => { event = MouseEvent::WheelDown;
let event = MouseEvent::Hold(self.last_button.unwrap()); position = (x, y).into();
let position = (x, y).into();
Event::Mouse {
event,
position,
offset: Vec2::zero(),
} }
CMouseEvent::ScrollUp(x, y, _) => {
event = MouseEvent::WheelDown;
position = (x, y).into();
}
};
Event::Mouse {
event,
position,
offset: Vec2::zero(),
} }
_ => {
log::warn!(
"Unknown mouse button event {:?}!",
mouse_event
);
Event::Unknown(vec![])
}
},
_ => {
log::warn!("Unknown mouse event {:?}!", event);
Event::Unknown(vec![])
} }
CEvent::Resize(_, _) => Event::WindowResize,
} }
} }
} }
impl backend::Backend for Backend { impl backend::Backend for Backend {
fn name(&self) -> &str {
"crossterm"
}
fn poll_event(&mut self) -> Option<Event> { fn poll_event(&mut self) -> Option<Event> {
self.async_reader.next().map(|event| self.map_key(event)) match poll(Duration::from_millis(1)) {
Ok(true) => match read() {
Ok(event) => Some(self.map_key(event)),
Err(e) => panic!("{:?}", e),
},
_ => None,
}
} }
fn finish(&mut self) { fn finish(&mut self) {
input().disable_mouse_mode().unwrap(); // We have to execute the show cursor command at the `stdout`.
cursor().show().unwrap(); execute!(
io::stdout(),
LeaveAlternateScreen,
DisableMouseCapture,
Show
)
.expect("Can not disable mouse capture or show cursor.");
disable_raw_mode().unwrap();
} }
fn refresh(&mut self) { fn refresh(&mut self) {
self.stdout.borrow_mut().flush().unwrap(); self.stdout_mut().flush().unwrap();
} }
fn has_colors(&self) -> bool { fn has_colors(&self) -> bool {
@ -187,24 +325,25 @@ impl backend::Backend for Backend {
} }
fn screen_size(&self) -> Vec2 { fn screen_size(&self) -> Vec2 {
let size = self.terminal.size().unwrap_or((0, 0)); let size = terminal::size().unwrap_or((1, 1));
Vec2::from(size) Vec2::from(size)
} }
fn print_at(&self, pos: Vec2, text: &str) { fn print_at(&self, pos: Vec2, text: &str) {
queue!(self.stdout.borrow_mut(), Goto(pos.x as u16, pos.y as u16)) queue!(
.unwrap(); self.stdout_mut(),
self.write(text); MoveTo(pos.x as u16, pos.y as u16),
Print(text)
)
.unwrap();
} }
fn print_at_rep(&self, pos: Vec2, repetitions: usize, text: &str) { fn print_at_rep(&self, pos: Vec2, repetitions: usize, text: &str) {
if repetitions > 0 { if repetitions > 0 {
let mut out = self.stdout.borrow_mut(); let mut out = self.stdout_mut();
queue!(out, Goto(pos.x as u16, pos.y as u16)).unwrap(); queue!(out, MoveTo(pos.x as u16, pos.y as u16)).unwrap();
// as I (Timon) wrote this I figured out that calling `write_str` for unix was flushing the stdout.
// Current work aground is writing bytes instead of a string to the terminal.
out.write_all(text.as_bytes()).unwrap(); out.write_all(text.as_bytes()).unwrap();
let mut dupes_left = repetitions - 1; let mut dupes_left = repetitions - 1;
@ -221,7 +360,7 @@ impl backend::Backend for Backend {
back: color, back: color,
}); });
queue!(self.stdout.borrow_mut(), Clear(ClearType::All)).unwrap(); queue!(self.stdout_mut(), Clear(ClearType::All)).unwrap();
} }
fn set_color(&self, color: theme::ColorPair) -> theme::ColorPair { fn set_color(&self, color: theme::ColorPair) -> theme::ColorPair {
@ -251,7 +390,7 @@ impl backend::Backend for Backend {
fn unset_effect(&self, effect: theme::Effect) { fn unset_effect(&self, effect: theme::Effect) {
match effect { match effect {
theme::Effect::Simple => (), theme::Effect::Simple => (),
theme::Effect::Reverse => self.set_attr(Attribute::NoInverse), theme::Effect::Reverse => self.set_attr(Attribute::NoReverse),
theme::Effect::Bold => self.set_attr(Attribute::NormalIntensity), theme::Effect::Bold => self.set_attr(Attribute::NormalIntensity),
theme::Effect::Italic => self.set_attr(Attribute::NoItalic), theme::Effect::Italic => self.set_attr(Attribute::NoItalic),
theme::Effect::Strikethrough => { theme::Effect::Strikethrough => {
@ -260,52 +399,8 @@ impl backend::Backend for Backend {
theme::Effect::Underline => self.set_attr(Attribute::NoUnderline), theme::Effect::Underline => self.set_attr(Attribute::NoUnderline),
} }
} }
}
fn with_color<F, R>(clr: theme::Color, f: F) -> R fn name(&self) -> &str {
where "crossterm"
F: FnOnce(&Color) -> R,
{
match clr {
theme::Color::Dark(theme::BaseColor::Black) => f(&Color::Black),
theme::Color::Dark(theme::BaseColor::Red) => f(&Color::DarkRed),
theme::Color::Dark(theme::BaseColor::Green) => f(&Color::DarkGreen),
theme::Color::Dark(theme::BaseColor::Yellow) => f(&Color::DarkYellow),
theme::Color::Dark(theme::BaseColor::Blue) => f(&Color::DarkBlue),
theme::Color::Dark(theme::BaseColor::Magenta) => {
f(&Color::DarkMagenta)
}
theme::Color::Dark(theme::BaseColor::Cyan) => f(&Color::DarkCyan),
theme::Color::Dark(theme::BaseColor::White) => f(&Color::Grey),
theme::Color::Light(theme::BaseColor::Black) => f(&Color::Grey),
theme::Color::Light(theme::BaseColor::Red) => f(&Color::Red),
theme::Color::Light(theme::BaseColor::Green) => f(&Color::Green),
theme::Color::Light(theme::BaseColor::Yellow) => f(&Color::Yellow),
theme::Color::Light(theme::BaseColor::Blue) => f(&Color::Blue),
theme::Color::Light(theme::BaseColor::Magenta) => f(&Color::Magenta),
theme::Color::Light(theme::BaseColor::Cyan) => f(&Color::Cyan),
theme::Color::Light(theme::BaseColor::White) => f(&Color::White),
theme::Color::Rgb(r, g, b) => f(&Color::Rgb { r, g, b }),
theme::Color::RgbLowRes(r, g, b) => {
debug_assert!(r <= 5,
"Red color fragment (r = {}) is out of bound. Make sure r ≤ 5.",
r);
debug_assert!(g <= 5,
"Green color fragment (g = {}) is out of bound. Make sure g ≤ 5.",
g);
debug_assert!(b <= 5,
"Blue color fragment (b = {}) is out of bound. Make sure b ≤ 5.",
b);
f(&Color::AnsiValue(16 + 36 * r + 6 * g + b))
}
theme::Color::TerminalDefault => {
unimplemented!(
"I have to take a look at how reset has to work out"
);
}
} }
} }