mirror of
https://github.com/FliegendeWurst/cursive.git
synced 2024-11-09 19:00:46 +00:00
Simplifies backend input
This commit is contained in:
parent
9bcbda4e7b
commit
88d2fb1f46
10
CHANGELOG.md
10
CHANGELOG.md
@ -6,9 +6,19 @@
|
|||||||
|
|
||||||
- Breaking change: `Finder::find_id()` is renamed to `call_on_id()`, and a proper
|
- Breaking change: `Finder::find_id()` is renamed to `call_on_id()`, and a proper
|
||||||
`find_id()` was added instead.
|
`find_id()` was added instead.
|
||||||
|
- Breaking change: replaced `set_fps(i32)` with `set_autorefresh(bool)`
|
||||||
|
- Breaking change: updated the Backend trait for a simpler input system
|
||||||
|
- Add a logging implementation (`logger::init()`) and a `DebugConsole`
|
||||||
|
(`cursive::toggle_debug_console()`)
|
||||||
- Add `StackView::remove_layer()`
|
- Add `StackView::remove_layer()`
|
||||||
- Add `CircularFocus` view (and bring proper circular focus to dialogs)
|
- Add `CircularFocus` view (and bring proper circular focus to dialogs)
|
||||||
- Add `HideableView::is_visible()`
|
- Add `HideableView::is_visible()`
|
||||||
|
- Add `type CbSink = Sender<Box<CbFunc>>` as an alias for the return type of
|
||||||
|
`Cursive::cb_sink()`
|
||||||
|
|
||||||
|
### Improvements
|
||||||
|
|
||||||
|
- Updated termion backend to use direct /dev/tty access for improved performance.
|
||||||
|
|
||||||
## 0.10.0
|
## 0.10.0
|
||||||
|
|
||||||
|
@ -16,8 +16,9 @@ fn main() {
|
|||||||
// As usual, create the Cursive root
|
// As usual, create the Cursive root
|
||||||
let mut siv = Cursive::default();
|
let mut siv = Cursive::default();
|
||||||
|
|
||||||
|
let cb_sink = siv.cb_sink().clone();
|
||||||
|
|
||||||
// We want to refresh the page even when no input is given.
|
// We want to refresh the page even when no input is given.
|
||||||
siv.set_fps(10);
|
|
||||||
siv.add_global_callback('q', |s| s.quit());
|
siv.add_global_callback('q', |s| s.quit());
|
||||||
|
|
||||||
// A channel will communicate data from our running task to the UI.
|
// A channel will communicate data from our running task to the UI.
|
||||||
@ -25,7 +26,7 @@ fn main() {
|
|||||||
|
|
||||||
// Generate data in a separate thread.
|
// Generate data in a separate thread.
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
generate_logs(&tx);
|
generate_logs(&tx, cb_sink);
|
||||||
});
|
});
|
||||||
|
|
||||||
// And sets the view to read from the other end of the channel.
|
// And sets the view to read from the other end of the channel.
|
||||||
@ -36,7 +37,7 @@ fn main() {
|
|||||||
|
|
||||||
// We will only simulate log generation here.
|
// We will only simulate log generation here.
|
||||||
// In real life, this may come from a running task, a separate process, ...
|
// In real life, this may come from a running task, a separate process, ...
|
||||||
fn generate_logs(tx: &mpsc::Sender<String>) {
|
fn generate_logs(tx: &mpsc::Sender<String>, cb_sink: cursive::CbSink) {
|
||||||
let mut i = 1;
|
let mut i = 1;
|
||||||
loop {
|
loop {
|
||||||
let line = format!("Interesting log line {}", i);
|
let line = format!("Interesting log line {}", i);
|
||||||
@ -46,6 +47,7 @@ fn generate_logs(tx: &mpsc::Sender<String>) {
|
|||||||
if tx.send(line).is_err() {
|
if tx.send(line).is_err() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
cb_sink.send(Box::new(Cursive::noop)).unwrap();
|
||||||
thread::sleep(Duration::from_millis(30));
|
thread::sleep(Duration::from_millis(30));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,9 +27,6 @@ fn main() {
|
|||||||
.content(Button::new("Start", phase_1)),
|
.content(Button::new("Start", phase_1)),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Auto-refresh is required for animated views
|
|
||||||
siv.set_fps(30);
|
|
||||||
|
|
||||||
siv.run();
|
siv.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,10 +53,12 @@ fn phase_1(s: &mut Cursive) {
|
|||||||
})
|
})
|
||||||
.full_width(),
|
.full_width(),
|
||||||
));
|
));
|
||||||
|
s.set_autorefresh(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn coffee_break(s: &mut Cursive) {
|
fn coffee_break(s: &mut Cursive) {
|
||||||
// A little break before things get serious.
|
// A little break before things get serious.
|
||||||
|
s.set_autorefresh(false);
|
||||||
s.pop_layer();
|
s.pop_layer();
|
||||||
s.add_layer(
|
s.add_layer(
|
||||||
Dialog::new()
|
Dialog::new()
|
||||||
@ -112,10 +111,12 @@ fn phase_2(s: &mut Cursive) {
|
|||||||
|
|
||||||
cb.send(Box::new(final_step)).unwrap();
|
cb.send(Box::new(final_step)).unwrap();
|
||||||
});
|
});
|
||||||
|
s.set_autorefresh(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn final_step(s: &mut Cursive) {
|
fn final_step(s: &mut Cursive) {
|
||||||
// A little break before things get serious.
|
// A little break before things get serious.
|
||||||
|
s.set_autorefresh(false);
|
||||||
s.pop_layer();
|
s.pop_layer();
|
||||||
s.add_layer(
|
s.add_layer(
|
||||||
Dialog::new()
|
Dialog::new()
|
||||||
|
@ -128,7 +128,7 @@ fn main() {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
siv.set_fps(10);
|
siv.set_autorefresh(true);
|
||||||
|
|
||||||
siv.run();
|
siv.run();
|
||||||
}
|
}
|
||||||
|
@ -6,15 +6,12 @@
|
|||||||
extern crate bear_lib_terminal;
|
extern crate bear_lib_terminal;
|
||||||
|
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::thread;
|
|
||||||
use std::time::{Duration, Instant};
|
|
||||||
|
|
||||||
use self::bear_lib_terminal::geometry::Size;
|
use self::bear_lib_terminal::geometry::Size;
|
||||||
use self::bear_lib_terminal::terminal::{
|
use self::bear_lib_terminal::terminal::{
|
||||||
self, state, Event as BltEvent, KeyCode,
|
self, state, Event as BltEvent, KeyCode,
|
||||||
};
|
};
|
||||||
use self::bear_lib_terminal::Color as BltColor;
|
use self::bear_lib_terminal::Color as BltColor;
|
||||||
use crossbeam_channel::{self, Receiver, Sender};
|
|
||||||
|
|
||||||
use backend;
|
use backend;
|
||||||
use event::{Event, Key, MouseButton, MouseEvent};
|
use event::{Event, Key, MouseButton, MouseEvent};
|
||||||
@ -30,9 +27,6 @@ enum ColorRole {
|
|||||||
pub struct Backend {
|
pub struct Backend {
|
||||||
buttons_pressed: HashSet<MouseButton>,
|
buttons_pressed: HashSet<MouseButton>,
|
||||||
mouse_position: Vec2,
|
mouse_position: Vec2,
|
||||||
|
|
||||||
inner_sender: Sender<Option<Event>>,
|
|
||||||
inner_receiver: Receiver<Option<Event>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Backend {
|
impl Backend {
|
||||||
@ -51,13 +45,9 @@ impl Backend {
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let (inner_sender, inner_receiver) = crossbeam_channel::bounded(1);
|
|
||||||
|
|
||||||
let c = Backend {
|
let c = Backend {
|
||||||
buttons_pressed: HashSet::new(),
|
buttons_pressed: HashSet::new(),
|
||||||
mouse_position: Vec2::zero(),
|
mouse_position: Vec2::zero(),
|
||||||
inner_sender,
|
|
||||||
inner_receiver,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Box::new(c)
|
Box::new(c)
|
||||||
@ -320,45 +310,8 @@ impl backend::Backend for Backend {
|
|||||||
terminal::print_xy(pos.x as i32, pos.y as i32, text);
|
terminal::print_xy(pos.x as i32, pos.y as i32, text);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn start_input_thread(
|
fn poll_event(&mut self) -> Option<Event> {
|
||||||
&mut self, event_sink: Sender<Option<Event>>,
|
self.parse_next()
|
||||||
input_requests: Receiver<backend::InputRequest>,
|
|
||||||
) {
|
|
||||||
let receiver = self.inner_receiver.clone();
|
|
||||||
|
|
||||||
thread::spawn(move || {
|
|
||||||
for _ in input_requests {
|
|
||||||
match receiver.recv() {
|
|
||||||
Err(_) => return,
|
|
||||||
Ok(event) => {
|
|
||||||
if event_sink.send(event).is_err() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prepare_input(&mut self, input_request: backend::InputRequest) {
|
|
||||||
match input_request {
|
|
||||||
backend::InputRequest::Peek => {
|
|
||||||
let event = self.parse_next();
|
|
||||||
self.inner_sender.send(event).unwrap();
|
|
||||||
}
|
|
||||||
backend::InputRequest::Block => {
|
|
||||||
let timeout = Duration::from_millis(30);
|
|
||||||
// Wait for up to `timeout_ms`.
|
|
||||||
let start = Instant::now();
|
|
||||||
while start.elapsed() < timeout {
|
|
||||||
if let Some(event) = self.parse_next() {
|
|
||||||
self.inner_sender.send(Some(event)).unwrap();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.inner_sender.send(Some(Event::Refresh)).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,13 +3,10 @@
|
|||||||
//! Requires either of `ncurses-backend` or `pancurses-backend`.
|
//! Requires either of `ncurses-backend` or `pancurses-backend`.
|
||||||
#![cfg(any(feature = "ncurses-backend", feature = "pancurses-backend"))]
|
#![cfg(any(feature = "ncurses-backend", feature = "pancurses-backend"))]
|
||||||
|
|
||||||
extern crate term_size;
|
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use event::{Event, Key};
|
use event::{Event, Key};
|
||||||
use theme::{BaseColor, Color, ColorPair};
|
use theme::{BaseColor, Color, ColorPair};
|
||||||
use vec::Vec2;
|
|
||||||
|
|
||||||
#[cfg(feature = "ncurses-backend")]
|
#[cfg(feature = "ncurses-backend")]
|
||||||
pub mod n;
|
pub mod n;
|
||||||
@ -17,15 +14,6 @@ pub mod n;
|
|||||||
#[cfg(feature = "pancurses-backend")]
|
#[cfg(feature = "pancurses-backend")]
|
||||||
pub mod pan;
|
pub mod pan;
|
||||||
|
|
||||||
/// Get the size of the terminal.
|
|
||||||
///
|
|
||||||
/// Usually ncurses can do that by himself, but because we're playing with
|
|
||||||
/// threads, ncurses' signal handler is confused and he can't keep track of
|
|
||||||
/// the terminal size. Poor ncurses.
|
|
||||||
fn terminal_size() -> Vec2 {
|
|
||||||
term_size::dimensions().unwrap_or((0, 0)).into()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn split_i32(code: i32) -> Vec<u8> {
|
fn split_i32(code: i32) -> Vec<u8> {
|
||||||
(0..4).map(|i| ((code >> (8 * i)) & 0xFF) as u8).collect()
|
(0..4).map(|i| ((code >> (8 * i)) & 0xFF) as u8).collect()
|
||||||
}
|
}
|
||||||
@ -34,7 +22,7 @@ fn fill_key_codes<F>(target: &mut HashMap<i32, Event>, f: F)
|
|||||||
where
|
where
|
||||||
F: Fn(i32) -> Option<String>,
|
F: Fn(i32) -> Option<String>,
|
||||||
{
|
{
|
||||||
let key_names = hashmap!{
|
let key_names = hashmap! {
|
||||||
"DC" => Key::Del,
|
"DC" => Key::Del,
|
||||||
"DN" => Key::Down,
|
"DN" => Key::Down,
|
||||||
"END" => Key::End,
|
"END" => Key::End,
|
||||||
|
@ -7,13 +7,8 @@ use std::ffi::CString;
|
|||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
|
||||||
use std::sync::Arc;
|
|
||||||
use std::thread;
|
|
||||||
|
|
||||||
use crossbeam_channel::{Receiver, Sender};
|
|
||||||
use libc;
|
use libc;
|
||||||
use signal_hook::iterator::Signals;
|
|
||||||
|
|
||||||
use backend;
|
use backend;
|
||||||
use event::{Event, Key, MouseButton, MouseEvent};
|
use event::{Event, Key, MouseButton, MouseEvent};
|
||||||
@ -31,29 +26,137 @@ pub struct Backend {
|
|||||||
// Maps (front, back) ncurses colors to ncurses pairs
|
// Maps (front, back) ncurses colors to ncurses pairs
|
||||||
pairs: RefCell<HashMap<(i16, i16), i16>>,
|
pairs: RefCell<HashMap<(i16, i16), i16>>,
|
||||||
|
|
||||||
// This is set by the SIGWINCH-triggered thread.
|
// Pre-computed map of ncurses codes to parsed Event
|
||||||
// When TRUE, we should tell ncurses about the new terminal size.
|
|
||||||
needs_resize: Arc<AtomicBool>,
|
|
||||||
|
|
||||||
// The signal hook to receive SIGWINCH (window resize)
|
|
||||||
signals: Option<Signals>,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct InputParser {
|
|
||||||
key_codes: HashMap<i32, Event>,
|
key_codes: HashMap<i32, Event>,
|
||||||
|
|
||||||
|
// Remember the last pressed button to correctly feed Released Event
|
||||||
last_mouse_button: Option<MouseButton>,
|
last_mouse_button: Option<MouseButton>,
|
||||||
|
|
||||||
|
// Sometimes a code from ncurses should be split in two Events.
|
||||||
|
//
|
||||||
|
// So remember the one we didn't return.
|
||||||
input_buffer: Option<Event>,
|
input_buffer: Option<Event>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InputParser {
|
fn find_closest_pair(pair: ColorPair) -> (i16, i16) {
|
||||||
fn new() -> Self {
|
super::find_closest_pair(pair, ncurses::COLORS() as i16)
|
||||||
InputParser {
|
}
|
||||||
|
|
||||||
|
/// Writes some bytes directly to `/dev/tty`
|
||||||
|
///
|
||||||
|
/// Since this is not going to be used often, we can afford to re-open the
|
||||||
|
/// file every time.
|
||||||
|
fn write_to_tty(bytes: &[u8]) -> io::Result<()> {
|
||||||
|
let mut tty_output =
|
||||||
|
File::create("/dev/tty").expect("cursive can only run with a tty");
|
||||||
|
tty_output.write_all(bytes)?;
|
||||||
|
// tty_output will be flushed automatically at the end of the function.
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Backend {
|
||||||
|
/// Creates a new ncurses-based backend.
|
||||||
|
pub fn init() -> Box<backend::Backend> {
|
||||||
|
// Change the locale.
|
||||||
|
// For some reasons it's mandatory to get some UTF-8 support.
|
||||||
|
ncurses::setlocale(ncurses::LcCategory::all, "");
|
||||||
|
|
||||||
|
// The delay is the time ncurses wait after pressing ESC
|
||||||
|
// to see if it's an escape sequence.
|
||||||
|
// Default delay is way too long. 25 is imperceptible yet works fine.
|
||||||
|
::std::env::set_var("ESCDELAY", "25");
|
||||||
|
|
||||||
|
// Don't output to standard IO, directly feed into /dev/tty
|
||||||
|
// This leaves stdin and stdout usable for other purposes.
|
||||||
|
let tty_path = CString::new("/dev/tty").unwrap();
|
||||||
|
let mode = CString::new("r+").unwrap();
|
||||||
|
let tty = unsafe { libc::fopen(tty_path.as_ptr(), mode.as_ptr()) };
|
||||||
|
ncurses::newterm(None, tty, tty);
|
||||||
|
// Enable keypad (like arrows)
|
||||||
|
ncurses::keypad(ncurses::stdscr(), true);
|
||||||
|
|
||||||
|
// This disables mouse click detection,
|
||||||
|
// and provides 0-delay access to mouse presses.
|
||||||
|
ncurses::mouseinterval(0);
|
||||||
|
// Listen to all mouse events.
|
||||||
|
ncurses::mousemask(
|
||||||
|
(ncurses::ALL_MOUSE_EVENTS | ncurses::REPORT_MOUSE_POSITION)
|
||||||
|
as mmask_t,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
// Enable non-blocking input, so getch() immediately returns.
|
||||||
|
ncurses::timeout(0);
|
||||||
|
// Don't echo user input, we'll take care of that
|
||||||
|
ncurses::noecho();
|
||||||
|
// This disables buffering and some input processing.
|
||||||
|
// TODO: use ncurses::raw() ?
|
||||||
|
ncurses::cbreak();
|
||||||
|
// This enables color support.
|
||||||
|
ncurses::start_color();
|
||||||
|
// Pick up background and text color from the terminal theme.
|
||||||
|
ncurses::use_default_colors();
|
||||||
|
// Don't print cursors.
|
||||||
|
ncurses::curs_set(ncurses::CURSOR_VISIBILITY::CURSOR_INVISIBLE);
|
||||||
|
|
||||||
|
// This asks the terminal to provide us with mouse drag events
|
||||||
|
// (Mouse move when a button is pressed).
|
||||||
|
// Replacing 1002 with 1003 would give us ANY mouse move.
|
||||||
|
write_to_tty(b"\x1B[?1002h").unwrap();
|
||||||
|
|
||||||
|
let c = Backend {
|
||||||
|
current_style: Cell::new(ColorPair::from_256colors(0, 0)),
|
||||||
|
pairs: RefCell::new(HashMap::new()),
|
||||||
key_codes: initialize_keymap(),
|
key_codes: initialize_keymap(),
|
||||||
last_mouse_button: None,
|
last_mouse_button: None,
|
||||||
input_buffer: None,
|
input_buffer: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
Box::new(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save a new color pair.
|
||||||
|
fn insert_color(
|
||||||
|
&self, pairs: &mut HashMap<(i16, i16), i16>, (front, back): (i16, i16),
|
||||||
|
) -> i16 {
|
||||||
|
let n = 1 + pairs.len() as i16;
|
||||||
|
|
||||||
|
let target = if ncurses::COLOR_PAIRS() > i32::from(n) {
|
||||||
|
// We still have plenty of space for everyone.
|
||||||
|
n
|
||||||
|
} else {
|
||||||
|
// The world is too small for both of us.
|
||||||
|
let target = n - 1;
|
||||||
|
// Remove the mapping to n-1
|
||||||
|
pairs.retain(|_, &mut v| v != target);
|
||||||
|
target
|
||||||
|
};
|
||||||
|
pairs.insert((front, back), target);
|
||||||
|
ncurses::init_pair(target, front, back);
|
||||||
|
target
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks the pair in the cache, or re-define a color if needed.
|
||||||
|
fn get_or_create(&self, pair: ColorPair) -> i16 {
|
||||||
|
let mut pairs = self.pairs.borrow_mut();
|
||||||
|
|
||||||
|
// Find if we have this color in stock
|
||||||
|
let (front, back) = find_closest_pair(pair);
|
||||||
|
if pairs.contains_key(&(front, back)) {
|
||||||
|
// We got it!
|
||||||
|
pairs[&(front, back)]
|
||||||
|
} else {
|
||||||
|
self.insert_color(&mut *pairs, (front, back))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_colors(&self, pair: ColorPair) {
|
||||||
|
let i = self.get_or_create(pair);
|
||||||
|
|
||||||
|
self.current_style.set(pair);
|
||||||
|
let style = ncurses::COLOR_PAIR(i);
|
||||||
|
ncurses::attron(style);
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_next(&mut self) -> Option<Event> {
|
fn parse_next(&mut self) -> Option<Event> {
|
||||||
if let Some(event) = self.input_buffer.take() {
|
if let Some(event) = self.input_buffer.take() {
|
||||||
return Some(event);
|
return Some(event);
|
||||||
@ -61,6 +164,7 @@ impl InputParser {
|
|||||||
|
|
||||||
let ch: i32 = ncurses::getch();
|
let ch: i32 = ncurses::getch();
|
||||||
|
|
||||||
|
// Non-blocking input will return -1 as long as no input is available.
|
||||||
if ch == -1 {
|
if ch == -1 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@ -185,129 +289,6 @@ impl InputParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_closest_pair(pair: ColorPair) -> (i16, i16) {
|
|
||||||
super::find_closest_pair(pair, ncurses::COLORS() as i16)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Writes some bytes directly to `/dev/tty`
|
|
||||||
///
|
|
||||||
/// Since this is not going to be used often, we can afford to re-open the
|
|
||||||
/// file every time.
|
|
||||||
fn write_to_tty(bytes: &[u8]) -> io::Result<()> {
|
|
||||||
let mut tty_output =
|
|
||||||
File::create("/dev/tty").expect("cursive can only run with a tty");
|
|
||||||
tty_output.write_all(bytes)?;
|
|
||||||
// tty_output will be flushed automatically at the end of the function.
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Backend {
|
|
||||||
/// Creates a new ncurses-based backend.
|
|
||||||
pub fn init() -> Box<backend::Backend> {
|
|
||||||
let signals = Some(Signals::new(&[libc::SIGWINCH]).unwrap());
|
|
||||||
|
|
||||||
// Change the locale.
|
|
||||||
// For some reasons it's mandatory to get some UTF-8 support.
|
|
||||||
ncurses::setlocale(ncurses::LcCategory::all, "");
|
|
||||||
|
|
||||||
// The delay is the time ncurses wait after pressing ESC
|
|
||||||
// to see if it's an escape sequence.
|
|
||||||
// Default delay is way too long. 25 is imperceptible yet works fine.
|
|
||||||
::std::env::set_var("ESCDELAY", "25");
|
|
||||||
|
|
||||||
let tty_path = CString::new("/dev/tty").unwrap();
|
|
||||||
let mode = CString::new("r+").unwrap();
|
|
||||||
let tty = unsafe { libc::fopen(tty_path.as_ptr(), mode.as_ptr()) };
|
|
||||||
ncurses::newterm(None, tty, tty);
|
|
||||||
ncurses::keypad(ncurses::stdscr(), true);
|
|
||||||
|
|
||||||
// This disables mouse click detection,
|
|
||||||
// and provides 0-delay access to mouse presses.
|
|
||||||
ncurses::mouseinterval(0);
|
|
||||||
// Listen to all mouse events.
|
|
||||||
ncurses::mousemask(
|
|
||||||
(ncurses::ALL_MOUSE_EVENTS | ncurses::REPORT_MOUSE_POSITION)
|
|
||||||
as mmask_t,
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
ncurses::noecho();
|
|
||||||
ncurses::cbreak();
|
|
||||||
ncurses::start_color();
|
|
||||||
// Pick up background and text color from the terminal theme.
|
|
||||||
ncurses::use_default_colors();
|
|
||||||
// No cursor
|
|
||||||
ncurses::curs_set(ncurses::CURSOR_VISIBILITY::CURSOR_INVISIBLE);
|
|
||||||
|
|
||||||
// This asks the terminal to provide us with mouse drag events
|
|
||||||
// (Mouse move when a button is pressed).
|
|
||||||
// Replacing 1002 with 1003 would give us ANY mouse move.
|
|
||||||
write_to_tty(b"\x1B[?1002h").unwrap();
|
|
||||||
|
|
||||||
let c = Backend {
|
|
||||||
current_style: Cell::new(ColorPair::from_256colors(0, 0)),
|
|
||||||
pairs: RefCell::new(HashMap::new()),
|
|
||||||
needs_resize: Arc::new(AtomicBool::new(false)),
|
|
||||||
signals,
|
|
||||||
};
|
|
||||||
|
|
||||||
Box::new(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Save a new color pair.
|
|
||||||
fn insert_color(
|
|
||||||
&self, pairs: &mut HashMap<(i16, i16), i16>, (front, back): (i16, i16),
|
|
||||||
) -> i16 {
|
|
||||||
let n = 1 + pairs.len() as i16;
|
|
||||||
|
|
||||||
let target = if ncurses::COLOR_PAIRS() > i32::from(n) {
|
|
||||||
// We still have plenty of space for everyone.
|
|
||||||
n
|
|
||||||
} else {
|
|
||||||
// The world is too small for both of us.
|
|
||||||
let target = n - 1;
|
|
||||||
// Remove the mapping to n-1
|
|
||||||
pairs.retain(|_, &mut v| v != target);
|
|
||||||
target
|
|
||||||
};
|
|
||||||
pairs.insert((front, back), target);
|
|
||||||
ncurses::init_pair(target, front, back);
|
|
||||||
target
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks the pair in the cache, or re-define a color if needed.
|
|
||||||
fn get_or_create(&self, pair: ColorPair) -> i16 {
|
|
||||||
let mut pairs = self.pairs.borrow_mut();
|
|
||||||
|
|
||||||
// Find if we have this color in stock
|
|
||||||
let (front, back) = find_closest_pair(pair);
|
|
||||||
if pairs.contains_key(&(front, back)) {
|
|
||||||
// We got it!
|
|
||||||
pairs[&(front, back)]
|
|
||||||
} else {
|
|
||||||
self.insert_color(&mut *pairs, (front, back))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_colors(&self, pair: ColorPair) {
|
|
||||||
let i = self.get_or_create(pair);
|
|
||||||
|
|
||||||
self.current_style.set(pair);
|
|
||||||
let style = ncurses::COLOR_PAIR(i);
|
|
||||||
ncurses::attron(style);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Called when a resize event is detected.
|
|
||||||
///
|
|
||||||
/// We need to have ncurses update its representation of the screen.
|
|
||||||
fn on_resize() {
|
|
||||||
// Get size
|
|
||||||
let size = super::terminal_size();
|
|
||||||
|
|
||||||
// Send the size to ncurses
|
|
||||||
ncurses::resize_term(size.y as i32, size.x as i32);
|
|
||||||
}
|
|
||||||
|
|
||||||
impl backend::Backend for Backend {
|
impl backend::Backend for Backend {
|
||||||
fn screen_size(&self) -> Vec2 {
|
fn screen_size(&self) -> Vec2 {
|
||||||
let mut x: i32 = 0;
|
let mut x: i32 = 0;
|
||||||
@ -320,44 +301,8 @@ impl backend::Backend for Backend {
|
|||||||
ncurses::has_colors()
|
ncurses::has_colors()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn start_input_thread(
|
fn poll_event(&mut self) -> Option<Event> {
|
||||||
&mut self, event_sink: Sender<Option<Event>>,
|
self.parse_next()
|
||||||
input_request: Receiver<backend::InputRequest>,
|
|
||||||
) {
|
|
||||||
let running = Arc::new(AtomicBool::new(true));
|
|
||||||
|
|
||||||
backend::resize::start_resize_thread(
|
|
||||||
self.signals.take().unwrap(),
|
|
||||||
event_sink.clone(),
|
|
||||||
input_request.clone(),
|
|
||||||
Arc::clone(&running),
|
|
||||||
Some(Arc::clone(&self.needs_resize)),
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut parser = InputParser::new();
|
|
||||||
|
|
||||||
// This thread will take input from ncurses for each request.
|
|
||||||
thread::spawn(move || {
|
|
||||||
for req in input_request {
|
|
||||||
match req {
|
|
||||||
backend::InputRequest::Peek => {
|
|
||||||
// When peeking, we want an answer instantly from ncurses
|
|
||||||
ncurses::timeout(0);
|
|
||||||
}
|
|
||||||
backend::InputRequest::Block => {
|
|
||||||
ncurses::timeout(-1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do the actual polling & parsing.
|
|
||||||
if event_sink.send(parser.parse_next()).is_err() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// The request channel is closed, which means Cursive has been
|
|
||||||
// dropped, so stop the resize-detection thread as well.
|
|
||||||
running.store(false, Ordering::Relaxed);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn finish(&mut self) {
|
fn finish(&mut self) {
|
||||||
@ -398,10 +343,6 @@ impl backend::Backend for Backend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn clear(&self, color: Color) {
|
fn clear(&self, color: Color) {
|
||||||
if self.needs_resize.swap(false, Ordering::Relaxed) {
|
|
||||||
on_resize();
|
|
||||||
}
|
|
||||||
|
|
||||||
let id = self.get_or_create(ColorPair {
|
let id = self.get_or_create(ColorPair {
|
||||||
front: color,
|
front: color,
|
||||||
back: color,
|
back: color,
|
||||||
|
@ -4,16 +4,6 @@ extern crate pancurses;
|
|||||||
use std::cell::{Cell, RefCell};
|
use std::cell::{Cell, RefCell};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::io::{stdout, Write};
|
use std::io::{stdout, Write};
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
|
||||||
use std::sync::Arc;
|
|
||||||
use std::thread;
|
|
||||||
|
|
||||||
use crossbeam_channel::{Receiver, Sender};
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
use libc;
|
|
||||||
#[cfg(unix)]
|
|
||||||
use signal_hook::iterator::Signals;
|
|
||||||
|
|
||||||
use backend;
|
use backend;
|
||||||
use event::{Event, Key, MouseButton, MouseEvent};
|
use event::{Event, Key, MouseButton, MouseEvent};
|
||||||
@ -30,40 +20,97 @@ pub struct Backend {
|
|||||||
pairs: RefCell<HashMap<(i16, i16), i32>>,
|
pairs: RefCell<HashMap<(i16, i16), i32>>,
|
||||||
|
|
||||||
// pancurses needs a handle to the current window.
|
// pancurses needs a handle to the current window.
|
||||||
window: Arc<pancurses::Window>,
|
window: pancurses::Window,
|
||||||
|
|
||||||
// This is set by the SIGWINCH-triggered thread.
|
|
||||||
// When TRUE, we should tell ncurses about the new terminal size.
|
|
||||||
needs_resize: Arc<AtomicBool>,
|
|
||||||
|
|
||||||
// The signal hook to receive SIGWINCH (window resize)
|
|
||||||
#[cfg(unix)]
|
|
||||||
signals: Option<Signals>,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct InputParser {
|
|
||||||
key_codes: HashMap<i32, Event>,
|
key_codes: HashMap<i32, Event>,
|
||||||
last_mouse_button: Option<MouseButton>,
|
last_mouse_button: Option<MouseButton>,
|
||||||
input_buffer: Option<Event>,
|
input_buffer: Option<Event>,
|
||||||
window: Arc<pancurses::Window>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ncurses (and pancurses) are not thread-safe
|
fn find_closest_pair(pair: ColorPair) -> (i16, i16) {
|
||||||
// (writing from two threads might cause garbage).
|
super::find_closest_pair(pair, pancurses::COLORS() as i16)
|
||||||
// BUT it's probably fine to read while we write.
|
}
|
||||||
// So `InputParser` will only read, and `Backend` will mostly write.
|
|
||||||
unsafe impl Send for InputParser {}
|
|
||||||
|
|
||||||
impl InputParser {
|
impl Backend {
|
||||||
fn new(window: Arc<pancurses::Window>) -> Self {
|
/// Creates a new pancurses-based backend.
|
||||||
InputParser {
|
pub fn init() -> Box<backend::Backend> {
|
||||||
|
::std::env::set_var("ESCDELAY", "25");
|
||||||
|
|
||||||
|
let window = pancurses::initscr();
|
||||||
|
window.keypad(true);
|
||||||
|
window.timeout(0);
|
||||||
|
pancurses::noecho();
|
||||||
|
pancurses::cbreak();
|
||||||
|
pancurses::start_color();
|
||||||
|
pancurses::use_default_colors();
|
||||||
|
pancurses::curs_set(0);
|
||||||
|
pancurses::mouseinterval(0);
|
||||||
|
pancurses::mousemask(
|
||||||
|
pancurses::ALL_MOUSE_EVENTS | pancurses::REPORT_MOUSE_POSITION,
|
||||||
|
::std::ptr::null_mut(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// This asks the terminal to provide us with mouse drag events
|
||||||
|
// (Mouse move when a button is pressed).
|
||||||
|
// Replacing 1002 with 1003 would give us ANY mouse move.
|
||||||
|
print!("\x1B[?1002h");
|
||||||
|
stdout().flush().expect("could not flush stdout");
|
||||||
|
|
||||||
|
let c = Backend {
|
||||||
|
current_style: Cell::new(ColorPair::from_256colors(0, 0)),
|
||||||
|
pairs: RefCell::new(HashMap::new()),
|
||||||
key_codes: initialize_keymap(),
|
key_codes: initialize_keymap(),
|
||||||
last_mouse_button: None,
|
last_mouse_button: None,
|
||||||
input_buffer: None,
|
input_buffer: None,
|
||||||
window,
|
window,
|
||||||
|
};
|
||||||
|
|
||||||
|
Box::new(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save a new color pair.
|
||||||
|
fn insert_color(
|
||||||
|
&self, pairs: &mut HashMap<(i16, i16), i32>, (front, back): (i16, i16),
|
||||||
|
) -> i32 {
|
||||||
|
let n = 1 + pairs.len() as i32;
|
||||||
|
|
||||||
|
// TODO: when COLORS_PAIRS is available...
|
||||||
|
let target = if 256 > n {
|
||||||
|
// We still have plenty of space for everyone.
|
||||||
|
n
|
||||||
|
} else {
|
||||||
|
// The world is too small for both of us.
|
||||||
|
let target = n - 1;
|
||||||
|
// Remove the mapping to n-1
|
||||||
|
pairs.retain(|_, &mut v| v != target);
|
||||||
|
target
|
||||||
|
};
|
||||||
|
pairs.insert((front, back), target);
|
||||||
|
pancurses::init_pair(target as i16, front, back);
|
||||||
|
target
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks the pair in the cache, or re-define a color if needed.
|
||||||
|
fn get_or_create(&self, pair: ColorPair) -> i32 {
|
||||||
|
let mut pairs = self.pairs.borrow_mut();
|
||||||
|
let pair = find_closest_pair(pair);
|
||||||
|
|
||||||
|
// Find if we have this color in stock
|
||||||
|
if pairs.contains_key(&pair) {
|
||||||
|
// We got it!
|
||||||
|
pairs[&pair]
|
||||||
|
} else {
|
||||||
|
self.insert_color(&mut *pairs, pair)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_colors(&self, pair: ColorPair) {
|
||||||
|
let i = self.get_or_create(pair);
|
||||||
|
|
||||||
|
self.current_style.set(pair);
|
||||||
|
let style = pancurses::COLOR_PAIR(i as pancurses::chtype);
|
||||||
|
self.window.attron(style);
|
||||||
|
}
|
||||||
fn parse_next(&mut self) -> Option<Event> {
|
fn parse_next(&mut self) -> Option<Event> {
|
||||||
if let Some(event) = self.input_buffer.take() {
|
if let Some(event) = self.input_buffer.take() {
|
||||||
return Some(event);
|
return Some(event);
|
||||||
@ -284,107 +331,6 @@ impl InputParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_closest_pair(pair: ColorPair) -> (i16, i16) {
|
|
||||||
super::find_closest_pair(pair, pancurses::COLORS() as i16)
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Backend {
|
|
||||||
/// Creates a new pancurses-based backend.
|
|
||||||
pub fn init() -> Box<backend::Backend> {
|
|
||||||
// We need to create this now, before ncurses initialization
|
|
||||||
// Otherwise ncurses starts its own signal handling and it's a mess.
|
|
||||||
#[cfg(unix)]
|
|
||||||
let signals = Some(Signals::new(&[libc::SIGWINCH]).unwrap());
|
|
||||||
|
|
||||||
::std::env::set_var("ESCDELAY", "25");
|
|
||||||
|
|
||||||
let window = pancurses::initscr();
|
|
||||||
window.keypad(true);
|
|
||||||
pancurses::noecho();
|
|
||||||
pancurses::cbreak();
|
|
||||||
pancurses::start_color();
|
|
||||||
pancurses::use_default_colors();
|
|
||||||
pancurses::curs_set(0);
|
|
||||||
pancurses::mouseinterval(0);
|
|
||||||
pancurses::mousemask(
|
|
||||||
pancurses::ALL_MOUSE_EVENTS | pancurses::REPORT_MOUSE_POSITION,
|
|
||||||
::std::ptr::null_mut(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// This asks the terminal to provide us with mouse drag events
|
|
||||||
// (Mouse move when a button is pressed).
|
|
||||||
// Replacing 1002 with 1003 would give us ANY mouse move.
|
|
||||||
print!("\x1B[?1002h");
|
|
||||||
stdout().flush().expect("could not flush stdout");
|
|
||||||
|
|
||||||
let c = Backend {
|
|
||||||
current_style: Cell::new(ColorPair::from_256colors(0, 0)),
|
|
||||||
pairs: RefCell::new(HashMap::new()),
|
|
||||||
window: Arc::new(window),
|
|
||||||
needs_resize: Arc::new(AtomicBool::new(false)),
|
|
||||||
#[cfg(unix)]
|
|
||||||
signals,
|
|
||||||
};
|
|
||||||
|
|
||||||
Box::new(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Save a new color pair.
|
|
||||||
fn insert_color(
|
|
||||||
&self, pairs: &mut HashMap<(i16, i16), i32>, (front, back): (i16, i16),
|
|
||||||
) -> i32 {
|
|
||||||
let n = 1 + pairs.len() as i32;
|
|
||||||
|
|
||||||
// TODO: when COLORS_PAIRS is available...
|
|
||||||
let target = if 256 > n {
|
|
||||||
// We still have plenty of space for everyone.
|
|
||||||
n
|
|
||||||
} else {
|
|
||||||
// The world is too small for both of us.
|
|
||||||
let target = n - 1;
|
|
||||||
// Remove the mapping to n-1
|
|
||||||
pairs.retain(|_, &mut v| v != target);
|
|
||||||
target
|
|
||||||
};
|
|
||||||
pairs.insert((front, back), target);
|
|
||||||
pancurses::init_pair(target as i16, front, back);
|
|
||||||
target
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks the pair in the cache, or re-define a color if needed.
|
|
||||||
fn get_or_create(&self, pair: ColorPair) -> i32 {
|
|
||||||
let mut pairs = self.pairs.borrow_mut();
|
|
||||||
let pair = find_closest_pair(pair);
|
|
||||||
|
|
||||||
// Find if we have this color in stock
|
|
||||||
if pairs.contains_key(&pair) {
|
|
||||||
// We got it!
|
|
||||||
pairs[&pair]
|
|
||||||
} else {
|
|
||||||
self.insert_color(&mut *pairs, pair)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_colors(&self, pair: ColorPair) {
|
|
||||||
let i = self.get_or_create(pair);
|
|
||||||
|
|
||||||
self.current_style.set(pair);
|
|
||||||
let style = pancurses::COLOR_PAIR(i as pancurses::chtype);
|
|
||||||
self.window.attron(style);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Called when a resize event is detected.
|
|
||||||
///
|
|
||||||
/// We need to have ncurses update its representation of the screen.
|
|
||||||
fn on_resize() {
|
|
||||||
// Get size
|
|
||||||
let size = super::terminal_size();
|
|
||||||
|
|
||||||
// Send the size to ncurses
|
|
||||||
pancurses::resize_term(size.y as i32, size.x as i32);
|
|
||||||
}
|
|
||||||
|
|
||||||
impl backend::Backend for Backend {
|
impl backend::Backend for Backend {
|
||||||
fn screen_size(&self) -> Vec2 {
|
fn screen_size(&self) -> Vec2 {
|
||||||
// Coordinates are reversed here
|
// Coordinates are reversed here
|
||||||
@ -435,10 +381,6 @@ impl backend::Backend for Backend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn clear(&self, color: Color) {
|
fn clear(&self, color: Color) {
|
||||||
if self.needs_resize.swap(false, Ordering::Relaxed) {
|
|
||||||
on_resize();
|
|
||||||
}
|
|
||||||
|
|
||||||
let id = self.get_or_create(ColorPair {
|
let id = self.get_or_create(ColorPair {
|
||||||
front: color,
|
front: color,
|
||||||
back: color,
|
back: color,
|
||||||
@ -455,39 +397,8 @@ impl backend::Backend for Backend {
|
|||||||
self.window.mvaddstr(pos.y as i32, pos.x as i32, text);
|
self.window.mvaddstr(pos.y as i32, pos.x as i32, text);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn start_input_thread(
|
fn poll_event(&mut self) -> Option<Event> {
|
||||||
&mut self, event_sink: Sender<Option<Event>>,
|
self.parse_next()
|
||||||
input_request: Receiver<backend::InputRequest>,
|
|
||||||
) {
|
|
||||||
let running = Arc::new(AtomicBool::new(true));
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
{
|
|
||||||
backend::resize::start_resize_thread(
|
|
||||||
self.signals.take().unwrap(),
|
|
||||||
event_sink.clone(),
|
|
||||||
input_request.clone(),
|
|
||||||
Arc::clone(&running),
|
|
||||||
Some(Arc::clone(&self.needs_resize)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut input_parser = InputParser::new(Arc::clone(&self.window));
|
|
||||||
|
|
||||||
thread::spawn(move || {
|
|
||||||
for req in input_request {
|
|
||||||
match req {
|
|
||||||
backend::InputRequest::Peek => {
|
|
||||||
input_parser.window.timeout(0);
|
|
||||||
}
|
|
||||||
backend::InputRequest::Block => {
|
|
||||||
input_parser.window.timeout(-1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
event_sink.send(input_parser.parse_next()).unwrap();
|
|
||||||
}
|
|
||||||
running.store(false, Ordering::Relaxed);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,4 @@
|
|||||||
//! Dummy backend
|
//! Dummy backend
|
||||||
use std::thread;
|
|
||||||
|
|
||||||
use crossbeam_channel::{self, Receiver, Sender};
|
|
||||||
|
|
||||||
use backend;
|
use backend;
|
||||||
use event::Event;
|
use event::Event;
|
||||||
@ -11,10 +8,7 @@ use vec::Vec2;
|
|||||||
/// Dummy backend that does nothing and immediately exits.
|
/// Dummy backend that does nothing and immediately exits.
|
||||||
///
|
///
|
||||||
/// Mostly used for testing.
|
/// Mostly used for testing.
|
||||||
pub struct Backend {
|
pub struct Backend;
|
||||||
inner_sender: Sender<Option<Event>>,
|
|
||||||
inner_receiver: Receiver<Option<Event>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Backend {
|
impl Backend {
|
||||||
/// Creates a new dummy backend.
|
/// Creates a new dummy backend.
|
||||||
@ -22,11 +16,7 @@ impl Backend {
|
|||||||
where
|
where
|
||||||
Self: Sized,
|
Self: Sized,
|
||||||
{
|
{
|
||||||
let (inner_sender, inner_receiver) = crossbeam_channel::bounded(1);
|
Box::new(Backend)
|
||||||
Box::new(Backend {
|
|
||||||
inner_sender,
|
|
||||||
inner_receiver,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,29 +32,8 @@ impl backend::Backend for Backend {
|
|||||||
fn screen_size(&self) -> Vec2 {
|
fn screen_size(&self) -> Vec2 {
|
||||||
(1, 1).into()
|
(1, 1).into()
|
||||||
}
|
}
|
||||||
|
fn poll_event(&mut self) -> Option<Event> {
|
||||||
fn prepare_input(&mut self, _input_request: backend::InputRequest) {
|
Some(Event::Exit)
|
||||||
self.inner_sender.send(Some(Event::Exit)).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn start_input_thread(
|
|
||||||
&mut self, event_sink: Sender<Option<Event>>,
|
|
||||||
input_requests: Receiver<backend::InputRequest>,
|
|
||||||
) {
|
|
||||||
let receiver = self.inner_receiver.clone();
|
|
||||||
|
|
||||||
thread::spawn(move || {
|
|
||||||
for _ in input_requests {
|
|
||||||
match receiver.recv() {
|
|
||||||
Err(_) => return,
|
|
||||||
Ok(event) => {
|
|
||||||
if event_sink.send(event).is_err() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_at(&self, _: Vec2, _: &str) {}
|
fn print_at(&self, _: Vec2, _: &str) {}
|
||||||
|
@ -7,8 +7,6 @@
|
|||||||
//! using some common libraries. Each of those included backends needs a
|
//! using some common libraries. Each of those included backends needs a
|
||||||
//! corresonding feature to be enabled.
|
//! corresonding feature to be enabled.
|
||||||
|
|
||||||
use crossbeam_channel::{Receiver, Sender};
|
|
||||||
|
|
||||||
use event::Event;
|
use event::Event;
|
||||||
use theme;
|
use theme;
|
||||||
use vec::Vec2;
|
use vec::Vec2;
|
||||||
@ -22,16 +20,26 @@ pub mod blt;
|
|||||||
pub mod curses;
|
pub mod curses;
|
||||||
pub mod termion;
|
pub mod termion;
|
||||||
|
|
||||||
/// A request for input, sent to the backend.
|
|
||||||
pub enum InputRequest {
|
|
||||||
/// The backend should respond immediately with an answer, possibly empty.
|
|
||||||
Peek,
|
|
||||||
/// The backend should block until input is available.
|
|
||||||
Block,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Trait defining the required methods to be a backend.
|
/// Trait defining the required methods to be a backend.
|
||||||
|
///
|
||||||
|
/// A backend is the interface between the abstract view tree and the actual
|
||||||
|
/// input/output, like a terminal.
|
||||||
|
///
|
||||||
|
/// It usually delegates the work to a terminal-handling library like ncurses
|
||||||
|
/// or termion, or it can entirely simulate a terminal and show it as a
|
||||||
|
/// graphical window (BearLibTerminal).
|
||||||
|
///
|
||||||
|
/// When creating a new cursive tree with `Cursive::new()`, you will need to
|
||||||
|
/// provide a backend initializer - usually their `init()` function.
|
||||||
|
///
|
||||||
|
/// Backends are responsible for handling input and converting it to `Event`. Input must be
|
||||||
|
/// non-blocking, it will be polled regularly.
|
||||||
pub trait Backend {
|
pub trait Backend {
|
||||||
|
/// Polls the backend for any input.
|
||||||
|
///
|
||||||
|
/// Should return immediately.
|
||||||
|
fn poll_event(&mut self) -> Option<Event>;
|
||||||
|
|
||||||
// TODO: take `self` by value?
|
// TODO: take `self` by value?
|
||||||
// Or implement Drop?
|
// Or implement Drop?
|
||||||
/// Prepares to close the backend.
|
/// Prepares to close the backend.
|
||||||
@ -39,29 +47,12 @@ pub trait Backend {
|
|||||||
/// This should clear any state in the terminal.
|
/// This should clear any state in the terminal.
|
||||||
fn finish(&mut self);
|
fn finish(&mut self);
|
||||||
|
|
||||||
/// Starts a thread to collect input and send it to the given channel.
|
|
||||||
///
|
|
||||||
/// `event_trigger` will receive a value before any event is needed.
|
|
||||||
fn start_input_thread(
|
|
||||||
&mut self, event_sink: Sender<Option<Event>>,
|
|
||||||
input_request: Receiver<InputRequest>,
|
|
||||||
) {
|
|
||||||
// Dummy implementation for some backends.
|
|
||||||
let _ = event_sink;
|
|
||||||
let _ = input_request;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Prepares the backend to collect input.
|
|
||||||
///
|
|
||||||
/// This is only required for non-thread-safe backends like BearLibTerminal
|
|
||||||
/// where we cannot collect input in a separate thread.
|
|
||||||
fn prepare_input(&mut self, input_request: InputRequest) {
|
|
||||||
// Dummy implementation for most backends.
|
|
||||||
// Little trick to avoid unused variables.
|
|
||||||
let _ = input_request;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Refresh the screen.
|
/// Refresh the screen.
|
||||||
|
///
|
||||||
|
/// This will be called each frame after drawing has been done.
|
||||||
|
///
|
||||||
|
/// A backend could, for example, buffer any print command, and apply
|
||||||
|
/// everything when refresh() is called.
|
||||||
fn refresh(&mut self);
|
fn refresh(&mut self);
|
||||||
|
|
||||||
/// Should return `true` if this backend supports colors.
|
/// Should return `true` if this backend supports colors.
|
||||||
@ -79,9 +70,13 @@ pub trait Backend {
|
|||||||
/// Starts using a new color.
|
/// Starts using a new color.
|
||||||
///
|
///
|
||||||
/// This should return the previously active color.
|
/// This should return the previously active color.
|
||||||
|
///
|
||||||
|
/// Any call to `print_at` from now on should use the given color.
|
||||||
fn set_color(&self, colors: theme::ColorPair) -> theme::ColorPair;
|
fn set_color(&self, colors: theme::ColorPair) -> theme::ColorPair;
|
||||||
|
|
||||||
/// Enables the given effect.
|
/// Enables the given effect.
|
||||||
|
///
|
||||||
|
/// Any call to `print_at` from now on should use the given effect.
|
||||||
fn set_effect(&self, effect: theme::Effect);
|
fn set_effect(&self, effect: theme::Effect);
|
||||||
|
|
||||||
/// Disables the given effect.
|
/// Disables the given effect.
|
||||||
|
@ -2,53 +2,21 @@ use std::sync::atomic::{AtomicBool, Ordering};
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
|
|
||||||
use crossbeam_channel::{Receiver, Sender};
|
use crossbeam_channel::Sender;
|
||||||
use signal_hook::iterator::Signals;
|
use signal_hook::iterator::Signals;
|
||||||
|
|
||||||
use backend::InputRequest;
|
|
||||||
use event::Event;
|
|
||||||
|
|
||||||
/// This starts a new thread to listen for SIGWINCH signals
|
/// This starts a new thread to listen for SIGWINCH signals
|
||||||
///
|
#[allow(unused)]
|
||||||
/// As long as `resize_running` is true, it will listen for SIGWINCH, and,
|
|
||||||
/// when detected, it wil set `needs_resize` to true and send an event to
|
|
||||||
/// `resize_sender`. It will also consume an event from `resize_requests`
|
|
||||||
/// afterward, to keep the balance in the force.
|
|
||||||
#[cfg(unix)]
|
|
||||||
pub fn start_resize_thread(
|
pub fn start_resize_thread(
|
||||||
signals: Signals, resize_sender: Sender<Option<Event>>,
|
resize_sender: Sender<()>, resize_running: Arc<AtomicBool>,
|
||||||
resize_requests: Receiver<InputRequest>, resize_running: Arc<AtomicBool>,
|
|
||||||
needs_resize: Option<Arc<AtomicBool>>,
|
|
||||||
) {
|
) {
|
||||||
|
let signals = Signals::new(&[libc::SIGWINCH]).unwrap();
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
// This thread will listen to SIGWINCH events and report them.
|
// This thread will listen to SIGWINCH events and report them.
|
||||||
while resize_running.load(Ordering::Relaxed) {
|
while resize_running.load(Ordering::Relaxed) {
|
||||||
// We know it will only contain SIGWINCH signals, so no need to check.
|
// We know it will only contain SIGWINCH signals, so no need to check.
|
||||||
if signals.wait().count() > 0 {
|
if signals.wait().count() > 0 {
|
||||||
// Tell ncurses about the new terminal size.
|
resize_sender.send(()).unwrap();
|
||||||
// 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().
|
|
||||||
if let Some(ref needs_resize) = needs_resize {
|
|
||||||
needs_resize.store(true, Ordering::Relaxed);
|
|
||||||
}
|
|
||||||
|
|
||||||
resize_sender.send(Some(Event::WindowResize)).unwrap();
|
|
||||||
// We've sent the message.
|
|
||||||
// This means Cursive was listening, and will now soon be sending a new request.
|
|
||||||
// This means the input thread accepted a request, but hasn't sent a message yet.
|
|
||||||
// So we KNOW the input thread is not waiting for a new request.
|
|
||||||
|
|
||||||
// We sent an event for free, so pay for it now by consuming a request
|
|
||||||
while let Ok(InputRequest::Peek) = resize_requests.recv() {
|
|
||||||
// At this point Cursive will now listen for input.
|
|
||||||
// There is a chance the input thread will send his event before us.
|
|
||||||
// But without some extra atomic flag, it'd be hard to know.
|
|
||||||
// So instead, keep sending `None`
|
|
||||||
|
|
||||||
// Repeat until we receive a blocking call
|
|
||||||
resize_sender.send(None).unwrap();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -14,101 +14,94 @@ use self::termion::input::{MouseTerminal, TermRead};
|
|||||||
use self::termion::raw::{IntoRawMode, RawTerminal};
|
use self::termion::raw::{IntoRawMode, RawTerminal};
|
||||||
use self::termion::screen::AlternateScreen;
|
use self::termion::screen::AlternateScreen;
|
||||||
use self::termion::style as tstyle;
|
use self::termion::style as tstyle;
|
||||||
use crossbeam_channel::{self, Receiver, Sender};
|
use crossbeam_channel::{self, Receiver};
|
||||||
use libc;
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
use signal_hook::iterator::Signals;
|
|
||||||
|
|
||||||
use backend;
|
use backend;
|
||||||
use event::{Event, Key, MouseButton, MouseEvent};
|
use event::{Event, Key, MouseButton, MouseEvent};
|
||||||
use theme;
|
use theme;
|
||||||
use vec::Vec2;
|
use vec::Vec2;
|
||||||
|
|
||||||
use std::cell::Cell;
|
use std::cell::{Cell, RefCell};
|
||||||
use std::io::{Stdout, Write};
|
use std::fs::File;
|
||||||
|
use std::io::{BufWriter, Write};
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
|
|
||||||
/// Backend using termion
|
/// Backend using termion
|
||||||
pub struct Backend {
|
pub struct Backend {
|
||||||
terminal: AlternateScreen<MouseTerminal<RawTerminal<Stdout>>>,
|
terminal:
|
||||||
|
RefCell<AlternateScreen<MouseTerminal<RawTerminal<BufWriter<File>>>>>,
|
||||||
current_style: Cell<theme::ColorPair>,
|
current_style: Cell<theme::ColorPair>,
|
||||||
}
|
|
||||||
|
|
||||||
struct InputParser {
|
|
||||||
// Inner state required to parse input
|
// Inner state required to parse input
|
||||||
last_button: Option<MouseButton>,
|
last_button: Option<MouseButton>,
|
||||||
|
|
||||||
event_due: bool,
|
input_receiver: Receiver<TEvent>,
|
||||||
requests: Sender<()>,
|
resize_receiver: Receiver<()>,
|
||||||
input: Receiver<TEvent>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InputParser {
|
impl Backend {
|
||||||
// Creates a non-blocking abstraction over the usual blocking input
|
/// Creates a new termion-based backend.
|
||||||
fn new() -> Self {
|
pub fn init() -> Box<backend::Backend> {
|
||||||
let (input_sender, input_receiver) = crossbeam_channel::bounded(0);
|
// Use a ~8MB buffer
|
||||||
let (request_sender, request_receiver) = crossbeam_channel::bounded(0);
|
// Should be enough for a single screen most of the time.
|
||||||
|
let terminal =
|
||||||
|
RefCell::new(AlternateScreen::from(MouseTerminal::from(
|
||||||
|
BufWriter::with_capacity(
|
||||||
|
8_000_000,
|
||||||
|
File::create("/dev/tty").unwrap(),
|
||||||
|
)
|
||||||
|
.into_raw_mode()
|
||||||
|
.unwrap(),
|
||||||
|
)));
|
||||||
|
|
||||||
// This thread will stop after an event when `InputParser` is dropped.
|
write!(terminal.borrow_mut(), "{}", termion::cursor::Hide).unwrap();
|
||||||
|
|
||||||
|
let (input_sender, input_receiver) = crossbeam_channel::unbounded();
|
||||||
|
let (resize_sender, resize_receiver) = crossbeam_channel::bounded(0);
|
||||||
|
|
||||||
|
let running = Arc::new(AtomicBool::new(true));
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
backend::resize::start_resize_thread(
|
||||||
|
resize_sender,
|
||||||
|
Arc::clone(&running),
|
||||||
|
);
|
||||||
|
|
||||||
|
// We want nonblocking input, but termion is blocking by default
|
||||||
|
// Read input from a separate thread
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
let stdin = ::std::io::stdin();
|
let input = std::fs::File::open("/dev/tty").unwrap();
|
||||||
let stdin = stdin.lock();
|
let mut events = input.events();
|
||||||
let mut events = stdin.events();
|
|
||||||
|
|
||||||
for _ in request_receiver {
|
// Take all the events we can
|
||||||
let event: Result<TEvent, ::std::io::Error> =
|
while let Some(Ok(event)) = events.next() {
|
||||||
events.next().unwrap();
|
// If we can't send, it means the receiving side closed,
|
||||||
|
// so just stop.
|
||||||
if input_sender.send(event.unwrap()).is_err() {
|
if input_sender.send(event).is_err() {
|
||||||
return;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
running.store(false, Ordering::Relaxed);
|
||||||
});
|
});
|
||||||
|
|
||||||
InputParser {
|
let c = Backend {
|
||||||
|
terminal,
|
||||||
|
current_style: Cell::new(theme::ColorPair::from_256colors(0, 0)),
|
||||||
|
|
||||||
last_button: None,
|
last_button: None,
|
||||||
input: input_receiver,
|
input_receiver,
|
||||||
requests: request_sender,
|
resize_receiver,
|
||||||
event_due: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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(()).unwrap();
|
|
||||||
self.event_due = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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.
|
Box::new(c)
|
||||||
self.event_due = false;
|
|
||||||
Some(self.map_key(input.unwrap()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn next_event(&mut self) -> Event {
|
fn apply_colors(&self, colors: theme::ColorPair) {
|
||||||
self.request();
|
with_color(&colors.front, |c| self.write(tcolor::Fg(c)));
|
||||||
|
with_color(&colors.back, |c| self.write(tcolor::Bg(c)));
|
||||||
let input = self.input.recv().unwrap();
|
|
||||||
self.event_due = false;
|
|
||||||
self.map_key(input)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_key(&mut self, event: TEvent) -> Event {
|
fn map_key(&mut self, event: TEvent) -> Event {
|
||||||
@ -184,68 +177,33 @@ impl InputParser {
|
|||||||
_ => Event::Unknown(vec![]),
|
_ => Event::Unknown(vec![]),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
trait Effectable {
|
fn write<T>(&self, content: T)
|
||||||
fn on(&self);
|
where
|
||||||
fn off(&self);
|
T: std::fmt::Display,
|
||||||
}
|
{
|
||||||
|
write!(self.terminal.borrow_mut(), "{}", content).unwrap();
|
||||||
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 {
|
|
||||||
/// Creates a new termion-based backend.
|
|
||||||
pub fn init() -> Box<backend::Backend> {
|
|
||||||
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)));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl backend::Backend for Backend {
|
impl backend::Backend for Backend {
|
||||||
fn finish(&mut self) {
|
fn finish(&mut self) {
|
||||||
print!("{}{}", termion::cursor::Show, termion::cursor::Goto(1, 1));
|
write!(
|
||||||
print!(
|
self.terminal.get_mut(),
|
||||||
|
"{}{}",
|
||||||
|
termion::cursor::Show,
|
||||||
|
termion::cursor::Goto(1, 1)
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
write!(
|
||||||
|
self.terminal.get_mut(),
|
||||||
"{}[49m{}[39m{}",
|
"{}[49m{}[39m{}",
|
||||||
27 as char,
|
27 as char,
|
||||||
27 as char,
|
27 as char,
|
||||||
termion::clear::All
|
termion::clear::All
|
||||||
);
|
)
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_color(&self, color: theme::ColorPair) -> theme::ColorPair {
|
fn set_color(&self, color: theme::ColorPair) -> theme::ColorPair {
|
||||||
@ -260,11 +218,23 @@ impl backend::Backend for Backend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn set_effect(&self, effect: theme::Effect) {
|
fn set_effect(&self, effect: theme::Effect) {
|
||||||
effect.on();
|
match effect {
|
||||||
|
theme::Effect::Simple => (),
|
||||||
|
theme::Effect::Reverse => self.write(tstyle::Invert),
|
||||||
|
theme::Effect::Bold => self.write(tstyle::Bold),
|
||||||
|
theme::Effect::Italic => self.write(tstyle::Italic),
|
||||||
|
theme::Effect::Underline => self.write(tstyle::Underline),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unset_effect(&self, effect: theme::Effect) {
|
fn unset_effect(&self, effect: theme::Effect) {
|
||||||
effect.off();
|
match effect {
|
||||||
|
theme::Effect::Simple => (),
|
||||||
|
theme::Effect::Reverse => self.write(tstyle::NoInvert),
|
||||||
|
theme::Effect::Bold => self.write(tstyle::NoBold),
|
||||||
|
theme::Effect::Italic => self.write(tstyle::NoItalic),
|
||||||
|
theme::Effect::Underline => self.write(tstyle::NoUnderline),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn has_colors(&self) -> bool {
|
fn has_colors(&self) -> bool {
|
||||||
@ -273,6 +243,8 @@ impl backend::Backend for Backend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn screen_size(&self) -> Vec2 {
|
fn screen_size(&self) -> Vec2 {
|
||||||
|
// TODO: termion::terminal_size currently requires stdout.
|
||||||
|
// When available, we should try to use /dev/tty instead.
|
||||||
let (x, y) = termion::terminal_size().unwrap_or((1, 1));
|
let (x, y) = termion::terminal_size().unwrap_or((1, 1));
|
||||||
(x, y).into()
|
(x, y).into()
|
||||||
}
|
}
|
||||||
@ -282,52 +254,31 @@ impl backend::Backend for Backend {
|
|||||||
front: color,
|
front: color,
|
||||||
back: color,
|
back: color,
|
||||||
});
|
});
|
||||||
print!("{}", termion::clear::All);
|
|
||||||
|
self.write(termion::clear::All);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn refresh(&mut self) {
|
fn refresh(&mut self) {
|
||||||
self.terminal.flush().unwrap();
|
self.terminal.get_mut().flush().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_at(&self, pos: Vec2, text: &str) {
|
fn print_at(&self, pos: Vec2, text: &str) {
|
||||||
print!(
|
write!(
|
||||||
|
self.terminal.borrow_mut(),
|
||||||
"{}{}",
|
"{}{}",
|
||||||
termion::cursor::Goto(1 + pos.x as u16, 1 + pos.y as u16),
|
termion::cursor::Goto(1 + pos.x as u16, 1 + pos.y as u16),
|
||||||
text
|
text
|
||||||
);
|
)
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn start_input_thread(
|
fn poll_event(&mut self) -> Option<Event> {
|
||||||
&mut self, event_sink: Sender<Option<Event>>,
|
let event = select! {
|
||||||
input_request: Receiver<backend::InputRequest>,
|
recv(self.input_receiver) -> event => event.ok(),
|
||||||
) {
|
recv(self.resize_receiver) -> _ => return Some(Event::WindowResize),
|
||||||
let running = Arc::new(AtomicBool::new(true));
|
default => return None,
|
||||||
|
};
|
||||||
#[cfg(unix)]
|
event.map(|event| self.map_key(event))
|
||||||
{
|
|
||||||
backend::resize::start_resize_thread(
|
|
||||||
Signals::new(&[libc::SIGWINCH]).unwrap(),
|
|
||||||
event_sink.clone(),
|
|
||||||
input_request.clone(),
|
|
||||||
Arc::clone(&running),
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut parser = InputParser::new();
|
|
||||||
thread::spawn(move || {
|
|
||||||
for req in input_request {
|
|
||||||
match req {
|
|
||||||
backend::InputRequest::Peek => {
|
|
||||||
event_sink.send(parser.peek()).unwrap();
|
|
||||||
}
|
|
||||||
backend::InputRequest::Block => {
|
|
||||||
event_sink.send(Some(parser.next_event())).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
running.store(false, Ordering::Relaxed);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
183
src/cursive.rs
183
src/cursive.rs
@ -33,7 +33,7 @@ pub struct Cursive {
|
|||||||
// If it changed, clear the screen.
|
// If it changed, clear the screen.
|
||||||
last_sizes: Vec<Vec2>,
|
last_sizes: Vec<Vec2>,
|
||||||
|
|
||||||
fps: u32,
|
autorefresh: bool,
|
||||||
|
|
||||||
active_screen: ScreenId,
|
active_screen: ScreenId,
|
||||||
|
|
||||||
@ -43,27 +43,14 @@ pub struct Cursive {
|
|||||||
|
|
||||||
cb_source: Receiver<Box<CbFunc>>,
|
cb_source: Receiver<Box<CbFunc>>,
|
||||||
cb_sink: Sender<Box<CbFunc>>,
|
cb_sink: Sender<Box<CbFunc>>,
|
||||||
|
|
||||||
event_source: Receiver<Option<Event>>,
|
|
||||||
|
|
||||||
// Sends true or false after each event.
|
|
||||||
input_trigger: Sender<backend::InputRequest>,
|
|
||||||
expecting_event: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Describes one of the possible interruptions we should handle.
|
|
||||||
enum Interruption {
|
|
||||||
/// An input event was received
|
|
||||||
Event(Event),
|
|
||||||
/// A callback was received
|
|
||||||
Callback(Box<CbFunc>),
|
|
||||||
/// A timeout ran out
|
|
||||||
Timeout,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Identifies a screen in the cursive root.
|
/// Identifies a screen in the cursive root.
|
||||||
pub type ScreenId = usize;
|
pub type ScreenId = usize;
|
||||||
|
|
||||||
|
/// Convenient alias to the result of `Cursive::cb_sink`.
|
||||||
|
pub type CbSink = Sender<Box<CbFunc>>;
|
||||||
|
|
||||||
/// Asynchronous callback function trait.
|
/// Asynchronous callback function trait.
|
||||||
///
|
///
|
||||||
/// Every `FnOnce(&mut Cursive) -> () + Send` automatically
|
/// Every `FnOnce(&mut Cursive) -> () + Send` automatically
|
||||||
@ -145,15 +132,11 @@ impl Cursive {
|
|||||||
let theme = theme::load_default();
|
let theme = theme::load_default();
|
||||||
|
|
||||||
let (cb_sink, cb_source) = crossbeam_channel::unbounded();
|
let (cb_sink, cb_source) = crossbeam_channel::unbounded();
|
||||||
let (event_sink, event_source) = crossbeam_channel::bounded(0);
|
|
||||||
|
|
||||||
let (input_sink, input_source) = crossbeam_channel::bounded(0);
|
let backend = backend_init();
|
||||||
|
|
||||||
let mut backend = backend_init();
|
|
||||||
backend.start_input_thread(event_sink, input_source);
|
|
||||||
|
|
||||||
Cursive {
|
Cursive {
|
||||||
fps: 0,
|
autorefresh: false,
|
||||||
theme,
|
theme,
|
||||||
screens: vec![views::StackView::new()],
|
screens: vec![views::StackView::new()],
|
||||||
last_sizes: Vec::new(),
|
last_sizes: Vec::new(),
|
||||||
@ -163,10 +146,7 @@ impl Cursive {
|
|||||||
running: true,
|
running: true,
|
||||||
cb_source,
|
cb_source,
|
||||||
cb_sink,
|
cb_sink,
|
||||||
event_source,
|
|
||||||
backend,
|
backend,
|
||||||
input_trigger: input_sink,
|
|
||||||
expecting_event: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -240,9 +220,6 @@ impl Cursive {
|
|||||||
/// Callbacks will be executed in the order
|
/// Callbacks will be executed in the order
|
||||||
/// of arrival on the next event cycle.
|
/// of arrival on the next event cycle.
|
||||||
///
|
///
|
||||||
/// Note that you currently need to call [`set_fps`] to force cursive to
|
|
||||||
/// regularly check for messages.
|
|
||||||
///
|
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
@ -250,15 +227,12 @@ impl Cursive {
|
|||||||
/// # use cursive::*;
|
/// # use cursive::*;
|
||||||
/// # fn main() {
|
/// # fn main() {
|
||||||
/// let mut siv = Cursive::dummy();
|
/// let mut siv = Cursive::dummy();
|
||||||
/// siv.set_fps(10);
|
|
||||||
///
|
///
|
||||||
/// // quit() will be called during the next event cycle
|
/// // quit() will be called during the next event cycle
|
||||||
/// siv.cb_sink().send(Box::new(|s: &mut Cursive| s.quit())).unwrap();
|
/// siv.cb_sink().send(Box::new(|s: &mut Cursive| s.quit())).unwrap();
|
||||||
/// # }
|
/// # }
|
||||||
/// ```
|
/// ```
|
||||||
///
|
pub fn cb_sink(&self) -> &CbSink {
|
||||||
/// [`set_fps`]: #method.set_fps
|
|
||||||
pub fn cb_sink(&self) -> &Sender<Box<CbFunc>> {
|
|
||||||
&self.cb_sink
|
&self.cb_sink
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -364,19 +338,11 @@ impl Cursive {
|
|||||||
theme::load_toml(content).map(|theme| self.set_theme(theme))
|
theme::load_toml(content).map(|theme| self.set_theme(theme))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the refresh rate, in frames per second.
|
/// Enables or disables automatic refresh of the screen.
|
||||||
///
|
///
|
||||||
/// Regularly redraws everything, even when no input is given.
|
/// When on, regularly redraws everything, even when no input is given.
|
||||||
///
|
pub fn set_autorefresh(&mut self, autorefresh: bool) {
|
||||||
/// You currently need this to regularly check
|
self.autorefresh = autorefresh;
|
||||||
/// for events sent using [`cb_sink`].
|
|
||||||
///
|
|
||||||
/// Between 0 and 1000. Call with `fps = 0` to disable (default value).
|
|
||||||
///
|
|
||||||
/// [`cb_sink`]: #method.cb_sink
|
|
||||||
pub fn set_fps(&mut self, fps: u32) {
|
|
||||||
// self.backend.set_refresh_rate(fps)
|
|
||||||
self.fps = fps;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a reference to the currently active screen.
|
/// Returns a reference to the currently active screen.
|
||||||
@ -649,63 +615,6 @@ impl Cursive {
|
|||||||
self.screen_mut().reposition_layer(layer, position);
|
self.screen_mut().reposition_layer(layer, position);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn peek(&mut self) -> Option<Interruption> {
|
|
||||||
// First, try a callback
|
|
||||||
select! {
|
|
||||||
// Skip to input if nothing is ready
|
|
||||||
default => (),
|
|
||||||
recv(self.cb_source) -> cb => return Some(Interruption::Callback(cb.unwrap())),
|
|
||||||
}
|
|
||||||
|
|
||||||
// No callback? Check input then
|
|
||||||
if self.expecting_event {
|
|
||||||
// We're already blocking.
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.input_trigger
|
|
||||||
.send(backend::InputRequest::Peek)
|
|
||||||
.unwrap();
|
|
||||||
self.backend.prepare_input(backend::InputRequest::Peek);
|
|
||||||
|
|
||||||
self.event_source.recv().unwrap().map(Interruption::Event)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Wait until something happens.
|
|
||||||
///
|
|
||||||
/// If `peek` is `true`, return `None` immediately if nothing is ready.
|
|
||||||
fn poll(&mut self) -> Option<Interruption> {
|
|
||||||
if !self.expecting_event {
|
|
||||||
self.input_trigger
|
|
||||||
.send(backend::InputRequest::Block)
|
|
||||||
.unwrap();
|
|
||||||
self.backend.prepare_input(backend::InputRequest::Block);
|
|
||||||
self.expecting_event = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
let timeout = if self.fps > 0 {
|
|
||||||
Duration::from_millis(1000 / self.fps as u64)
|
|
||||||
} else {
|
|
||||||
// Defaults to 1 refresh per hour.
|
|
||||||
Duration::from_secs(3600)
|
|
||||||
};
|
|
||||||
|
|
||||||
select! {
|
|
||||||
recv(self.event_source) -> event => {
|
|
||||||
// Ok, we processed the event.
|
|
||||||
self.expecting_event = false;
|
|
||||||
|
|
||||||
event.unwrap().map(Interruption::Event)
|
|
||||||
},
|
|
||||||
recv(self.cb_source) -> cb => {
|
|
||||||
cb.ok().map(Interruption::Callback)
|
|
||||||
},
|
|
||||||
recv(crossbeam_channel::after(timeout)) -> _ => {
|
|
||||||
Some(Interruption::Timeout)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handles a key event when it was ignored by the current view
|
// Handles a key event when it was ignored by the current view
|
||||||
fn on_ignored_event(&mut self, event: Event) {
|
fn on_ignored_event(&mut self, event: Event) {
|
||||||
let cb_list = match self.global_callbacks.get(&event) {
|
let cb_list = match self.global_callbacks.get(&event) {
|
||||||
@ -830,6 +739,8 @@ impl Cursive {
|
|||||||
pub fn run(&mut self) {
|
pub fn run(&mut self) {
|
||||||
self.running = true;
|
self.running = true;
|
||||||
|
|
||||||
|
self.refresh();
|
||||||
|
|
||||||
// And the big event loop begins!
|
// And the big event loop begins!
|
||||||
while self.running {
|
while self.running {
|
||||||
self.step();
|
self.step();
|
||||||
@ -843,6 +754,41 @@ impl Cursive {
|
|||||||
///
|
///
|
||||||
/// [`run(&mut self)`]: #method.run
|
/// [`run(&mut self)`]: #method.run
|
||||||
pub fn step(&mut self) {
|
pub fn step(&mut self) {
|
||||||
|
let mut boring = true;
|
||||||
|
|
||||||
|
// First, handle all available input
|
||||||
|
while let Some(event) = self.backend.poll_event() {
|
||||||
|
boring = false;
|
||||||
|
self.on_event(event);
|
||||||
|
|
||||||
|
if !self.running {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then, handle any available callback
|
||||||
|
while let Ok(cb) = self.cb_source.try_recv() {
|
||||||
|
boring = false;
|
||||||
|
cb.call_box(self);
|
||||||
|
|
||||||
|
if !self.running {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.autorefresh || !boring {
|
||||||
|
// Only re-draw if nothing happened.
|
||||||
|
self.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
if boring {
|
||||||
|
// Otherwise, sleep some more
|
||||||
|
std::thread::sleep(Duration::from_millis(30));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Refresh the screen with the current view tree state.
|
||||||
|
fn refresh(&mut self) {
|
||||||
// Do we need to redraw everytime?
|
// Do we need to redraw everytime?
|
||||||
// Probably, actually.
|
// Probably, actually.
|
||||||
// TODO: Do we need to re-layout everytime?
|
// TODO: Do we need to re-layout everytime?
|
||||||
@ -852,40 +798,17 @@ impl Cursive {
|
|||||||
// (Is this getting repetitive? :p)
|
// (Is this getting repetitive? :p)
|
||||||
self.draw();
|
self.draw();
|
||||||
self.backend.refresh();
|
self.backend.refresh();
|
||||||
|
|
||||||
if let Some(interruption) = self.poll() {
|
|
||||||
self.handle_interruption(interruption);
|
|
||||||
if !self.running {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't block, but try to read any other pending event.
|
|
||||||
// This lets us batch-process chunks of events, like big copy-paste or mouse drags.
|
|
||||||
while let Some(interruption) = self.peek() {
|
|
||||||
self.handle_interruption(interruption);
|
|
||||||
if !self.running {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_interruption(&mut self, interruption: Interruption) {
|
|
||||||
match interruption {
|
|
||||||
Interruption::Event(event) => {
|
|
||||||
self.on_event(event);
|
|
||||||
}
|
|
||||||
Interruption::Callback(cb) => {
|
|
||||||
cb.call_box(self);
|
|
||||||
}
|
|
||||||
Interruption::Timeout => {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stops the event loop.
|
/// Stops the event loop.
|
||||||
pub fn quit(&mut self) {
|
pub fn quit(&mut self) {
|
||||||
self.running = false;
|
self.running = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Does not do anything.
|
||||||
|
pub fn noop(&mut self) {
|
||||||
|
// foo
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for Cursive {
|
impl Drop for Cursive {
|
||||||
|
@ -129,7 +129,7 @@ mod utf8;
|
|||||||
|
|
||||||
pub mod backend;
|
pub mod backend;
|
||||||
|
|
||||||
pub use cursive::{CbFunc, Cursive, ScreenId};
|
pub use cursive::{CbFunc, CbSink, Cursive, ScreenId};
|
||||||
pub use printer::Printer;
|
pub use printer::Printer;
|
||||||
pub use vec::Vec2;
|
pub use vec::Vec2;
|
||||||
pub use with::With;
|
pub use with::With;
|
||||||
|
Loading…
Reference in New Issue
Block a user