diff --git a/CHANGELOG.md b/CHANGELOG.md index 0578f32..885ed97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,19 @@ - Breaking change: `Finder::find_id()` is renamed to `call_on_id()`, and a proper `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 `CircularFocus` view (and bring proper circular focus to dialogs) - Add `HideableView::is_visible()` +- Add `type CbSink = Sender>` 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 diff --git a/examples/logs.rs b/examples/logs.rs index 846fbf4..c710b0c 100644 --- a/examples/logs.rs +++ b/examples/logs.rs @@ -16,8 +16,9 @@ fn main() { // As usual, create the Cursive root let mut siv = Cursive::default(); + let cb_sink = siv.cb_sink().clone(); + // We want to refresh the page even when no input is given. - siv.set_fps(10); siv.add_global_callback('q', |s| s.quit()); // A channel will communicate data from our running task to the UI. @@ -25,7 +26,7 @@ fn main() { // Generate data in a separate thread. thread::spawn(move || { - generate_logs(&tx); + generate_logs(&tx, cb_sink); }); // 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. // In real life, this may come from a running task, a separate process, ... -fn generate_logs(tx: &mpsc::Sender) { +fn generate_logs(tx: &mpsc::Sender, cb_sink: cursive::CbSink) { let mut i = 1; loop { let line = format!("Interesting log line {}", i); @@ -46,6 +47,7 @@ fn generate_logs(tx: &mpsc::Sender) { if tx.send(line).is_err() { return; } + cb_sink.send(Box::new(Cursive::noop)).unwrap(); thread::sleep(Duration::from_millis(30)); } } diff --git a/examples/progress.rs b/examples/progress.rs index 21cd3cf..9a93898 100644 --- a/examples/progress.rs +++ b/examples/progress.rs @@ -27,9 +27,6 @@ fn main() { .content(Button::new("Start", phase_1)), ); - // Auto-refresh is required for animated views - siv.set_fps(30); - siv.run(); } @@ -56,10 +53,12 @@ fn phase_1(s: &mut Cursive) { }) .full_width(), )); + s.set_autorefresh(true); } fn coffee_break(s: &mut Cursive) { // A little break before things get serious. + s.set_autorefresh(false); s.pop_layer(); s.add_layer( Dialog::new() @@ -112,10 +111,12 @@ fn phase_2(s: &mut Cursive) { cb.send(Box::new(final_step)).unwrap(); }); + s.set_autorefresh(true); } fn final_step(s: &mut Cursive) { // A little break before things get serious. + s.set_autorefresh(false); s.pop_layer(); s.add_layer( Dialog::new() diff --git a/examples/vpv.rs b/examples/vpv.rs index 50b693d..1c69a65 100644 --- a/examples/vpv.rs +++ b/examples/vpv.rs @@ -128,7 +128,7 @@ fn main() { ), ); - siv.set_fps(10); + siv.set_autorefresh(true); siv.run(); } diff --git a/src/backend/blt.rs b/src/backend/blt.rs index afbdeb2..abdeb5a 100644 --- a/src/backend/blt.rs +++ b/src/backend/blt.rs @@ -6,15 +6,12 @@ extern crate bear_lib_terminal; use std::collections::HashSet; -use std::thread; -use std::time::{Duration, Instant}; use self::bear_lib_terminal::geometry::Size; use self::bear_lib_terminal::terminal::{ self, state, Event as BltEvent, KeyCode, }; use self::bear_lib_terminal::Color as BltColor; -use crossbeam_channel::{self, Receiver, Sender}; use backend; use event::{Event, Key, MouseButton, MouseEvent}; @@ -30,9 +27,6 @@ enum ColorRole { pub struct Backend { buttons_pressed: HashSet, mouse_position: Vec2, - - inner_sender: Sender>, - inner_receiver: Receiver>, } impl Backend { @@ -51,13 +45,9 @@ impl Backend { }, ]); - let (inner_sender, inner_receiver) = crossbeam_channel::bounded(1); - let c = Backend { buttons_pressed: HashSet::new(), mouse_position: Vec2::zero(), - inner_sender, - inner_receiver, }; Box::new(c) @@ -320,45 +310,8 @@ impl backend::Backend for Backend { terminal::print_xy(pos.x as i32, pos.y as i32, text); } - fn start_input_thread( - &mut self, event_sink: Sender>, - input_requests: Receiver, - ) { - 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(); - } - } + fn poll_event(&mut self) -> Option { + self.parse_next() } } diff --git a/src/backend/curses/mod.rs b/src/backend/curses/mod.rs index 83048ef..63cac1c 100644 --- a/src/backend/curses/mod.rs +++ b/src/backend/curses/mod.rs @@ -3,13 +3,10 @@ //! Requires either of `ncurses-backend` or `pancurses-backend`. #![cfg(any(feature = "ncurses-backend", feature = "pancurses-backend"))] -extern crate term_size; - use std::collections::HashMap; use event::{Event, Key}; use theme::{BaseColor, Color, ColorPair}; -use vec::Vec2; #[cfg(feature = "ncurses-backend")] pub mod n; @@ -17,15 +14,6 @@ pub mod n; #[cfg(feature = "pancurses-backend")] 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 { (0..4).map(|i| ((code >> (8 * i)) & 0xFF) as u8).collect() } @@ -34,7 +22,7 @@ fn fill_key_codes(target: &mut HashMap, f: F) where F: Fn(i32) -> Option, { - let key_names = hashmap!{ + let key_names = hashmap! { "DC" => Key::Del, "DN" => Key::Down, "END" => Key::End, diff --git a/src/backend/curses/n.rs b/src/backend/curses/n.rs index e7835bd..e489e8f 100644 --- a/src/backend/curses/n.rs +++ b/src/backend/curses/n.rs @@ -7,13 +7,8 @@ use std::ffi::CString; use std::fs::File; use std::io; 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 signal_hook::iterator::Signals; use backend; use event::{Event, Key, MouseButton, MouseEvent}; @@ -31,29 +26,137 @@ pub struct Backend { // Maps (front, back) ncurses colors to ncurses pairs pairs: RefCell>, - // This is set by the SIGWINCH-triggered thread. - // When TRUE, we should tell ncurses about the new terminal size. - needs_resize: Arc, - - // The signal hook to receive SIGWINCH (window resize) - signals: Option, -} - -struct InputParser { + // Pre-computed map of ncurses codes to parsed Event key_codes: HashMap, + + // Remember the last pressed button to correctly feed Released Event last_mouse_button: Option, + + // Sometimes a code from ncurses should be split in two Events. + // + // So remember the one we didn't return. input_buffer: Option, } -impl InputParser { - fn new() -> Self { - 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 { + // 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(), last_mouse_button: 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 { if let Some(event) = self.input_buffer.take() { return Some(event); @@ -61,6 +164,7 @@ impl InputParser { let ch: i32 = ncurses::getch(); + // Non-blocking input will return -1 as long as no input is available. if ch == -1 { 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 { - 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 { fn screen_size(&self) -> Vec2 { let mut x: i32 = 0; @@ -320,44 +301,8 @@ impl backend::Backend for Backend { ncurses::has_colors() } - fn start_input_thread( - &mut self, event_sink: Sender>, - input_request: Receiver, - ) { - 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 poll_event(&mut self) -> Option { + self.parse_next() } fn finish(&mut self) { @@ -398,10 +343,6 @@ impl backend::Backend for Backend { } fn clear(&self, color: Color) { - if self.needs_resize.swap(false, Ordering::Relaxed) { - on_resize(); - } - let id = self.get_or_create(ColorPair { front: color, back: color, diff --git a/src/backend/curses/pan.rs b/src/backend/curses/pan.rs index 3d1b560..53ed818 100644 --- a/src/backend/curses/pan.rs +++ b/src/backend/curses/pan.rs @@ -4,16 +4,6 @@ extern crate pancurses; use std::cell::{Cell, RefCell}; use std::collections::HashMap; 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 event::{Event, Key, MouseButton, MouseEvent}; @@ -30,40 +20,97 @@ pub struct Backend { pairs: RefCell>, // pancurses needs a handle to the current window. - window: Arc, + 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, - - // The signal hook to receive SIGWINCH (window resize) - #[cfg(unix)] - signals: Option, -} - -struct InputParser { key_codes: HashMap, last_mouse_button: Option, input_buffer: Option, - window: Arc, } -// Ncurses (and pancurses) are not thread-safe -// (writing from two threads might cause garbage). -// 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 {} +fn find_closest_pair(pair: ColorPair) -> (i16, i16) { + super::find_closest_pair(pair, pancurses::COLORS() as i16) +} -impl InputParser { - fn new(window: Arc) -> Self { - InputParser { +impl Backend { + /// Creates a new pancurses-based backend. + pub fn init() -> Box { + ::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(), last_mouse_button: None, input_buffer: None, 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 { if let Some(event) = self.input_buffer.take() { 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 { - // 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 { fn screen_size(&self) -> Vec2 { // Coordinates are reversed here @@ -435,10 +381,6 @@ impl backend::Backend for Backend { } fn clear(&self, color: Color) { - if self.needs_resize.swap(false, Ordering::Relaxed) { - on_resize(); - } - let id = self.get_or_create(ColorPair { front: color, back: color, @@ -455,39 +397,8 @@ impl backend::Backend for Backend { self.window.mvaddstr(pos.y as i32, pos.x as i32, text); } - fn start_input_thread( - &mut self, event_sink: Sender>, - input_request: Receiver, - ) { - 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); - }); + fn poll_event(&mut self) -> Option { + self.parse_next() } } diff --git a/src/backend/dummy.rs b/src/backend/dummy.rs index 4cd644a..1912031 100644 --- a/src/backend/dummy.rs +++ b/src/backend/dummy.rs @@ -1,7 +1,4 @@ //! Dummy backend -use std::thread; - -use crossbeam_channel::{self, Receiver, Sender}; use backend; use event::Event; @@ -11,10 +8,7 @@ use vec::Vec2; /// Dummy backend that does nothing and immediately exits. /// /// Mostly used for testing. -pub struct Backend { - inner_sender: Sender>, - inner_receiver: Receiver>, -} +pub struct Backend; impl Backend { /// Creates a new dummy backend. @@ -22,11 +16,7 @@ impl Backend { where Self: Sized, { - let (inner_sender, inner_receiver) = crossbeam_channel::bounded(1); - Box::new(Backend { - inner_sender, - inner_receiver, - }) + Box::new(Backend) } } @@ -42,29 +32,8 @@ impl backend::Backend for Backend { fn screen_size(&self) -> Vec2 { (1, 1).into() } - - fn prepare_input(&mut self, _input_request: backend::InputRequest) { - self.inner_sender.send(Some(Event::Exit)).unwrap(); - } - - fn start_input_thread( - &mut self, event_sink: Sender>, - input_requests: Receiver, - ) { - 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 poll_event(&mut self) -> Option { + Some(Event::Exit) } fn print_at(&self, _: Vec2, _: &str) {} diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 43a06f8..c86da26 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -7,8 +7,6 @@ //! using some common libraries. Each of those included backends needs a //! corresonding feature to be enabled. -use crossbeam_channel::{Receiver, Sender}; - use event::Event; use theme; use vec::Vec2; @@ -22,16 +20,26 @@ pub mod blt; pub mod curses; 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. +/// +/// 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 { + /// Polls the backend for any input. + /// + /// Should return immediately. + fn poll_event(&mut self) -> Option; + // TODO: take `self` by value? // Or implement Drop? /// Prepares to close the backend. @@ -39,29 +47,12 @@ pub trait Backend { /// This should clear any state in the terminal. 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>, - input_request: Receiver, - ) { - // 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. + /// + /// 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); /// Should return `true` if this backend supports colors. @@ -79,9 +70,13 @@ pub trait Backend { /// Starts using a new 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; /// Enables the given effect. + /// + /// Any call to `print_at` from now on should use the given effect. fn set_effect(&self, effect: theme::Effect); /// Disables the given effect. diff --git a/src/backend/resize.rs b/src/backend/resize.rs index 0695be5..9efe4bb 100644 --- a/src/backend/resize.rs +++ b/src/backend/resize.rs @@ -2,53 +2,21 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::thread; -use crossbeam_channel::{Receiver, Sender}; +use crossbeam_channel::Sender; use signal_hook::iterator::Signals; -use backend::InputRequest; -use event::Event; - /// This starts a new thread to listen for SIGWINCH signals -/// -/// 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)] +#[allow(unused)] pub fn start_resize_thread( - signals: Signals, resize_sender: Sender>, - resize_requests: Receiver, resize_running: Arc, - needs_resize: Option>, + resize_sender: Sender<()>, resize_running: Arc, ) { + let signals = Signals::new(&[libc::SIGWINCH]).unwrap(); thread::spawn(move || { // This thread will listen to SIGWINCH events and report them. while resize_running.load(Ordering::Relaxed) { // We know it will only contain SIGWINCH signals, so no need to check. if signals.wait().count() > 0 { - // 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(). - 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(); - } + resize_sender.send(()).unwrap(); } } }); diff --git a/src/backend/termion.rs b/src/backend/termion.rs index c2bc4f6..8a03ca4 100644 --- a/src/backend/termion.rs +++ b/src/backend/termion.rs @@ -14,101 +14,94 @@ use self::termion::input::{MouseTerminal, TermRead}; use self::termion::raw::{IntoRawMode, RawTerminal}; use self::termion::screen::AlternateScreen; use self::termion::style as tstyle; -use crossbeam_channel::{self, Receiver, Sender}; -use libc; - -#[cfg(unix)] -use signal_hook::iterator::Signals; +use crossbeam_channel::{self, Receiver}; use backend; use event::{Event, Key, MouseButton, MouseEvent}; use theme; use vec::Vec2; -use std::cell::Cell; -use std::io::{Stdout, Write}; +use std::cell::{Cell, RefCell}; +use std::fs::File; +use std::io::{BufWriter, Write}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::thread; /// Backend using termion pub struct Backend { - terminal: AlternateScreen>>, + terminal: + RefCell>>>>, current_style: Cell, -} -struct InputParser { // Inner state required to parse input last_button: Option, - event_due: bool, - requests: Sender<()>, - input: Receiver, + input_receiver: Receiver, + resize_receiver: Receiver<()>, } -impl InputParser { - // 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); +impl Backend { + /// Creates a new termion-based backend. + pub fn init() -> Box { + // Use a ~8MB buffer + // 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 || { - let stdin = ::std::io::stdin(); - let stdin = stdin.lock(); - let mut events = stdin.events(); + let input = std::fs::File::open("/dev/tty").unwrap(); + let mut events = input.events(); - for _ in request_receiver { - let event: Result = - events.next().unwrap(); - - if input_sender.send(event.unwrap()).is_err() { - return; + // Take all the events we can + while let Some(Ok(event)) = events.next() { + // If we can't send, it means the receiving side closed, + // so just stop. + if input_sender.send(event).is_err() { + break; } } + + running.store(false, Ordering::Relaxed); }); - InputParser { + let c = Backend { + terminal, + current_style: Cell::new(theme::ColorPair::from_256colors(0, 0)), + last_button: None, - input: input_receiver, - requests: request_sender, - 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 { - 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, + input_receiver, + resize_receiver, }; - // We got what we came for. - self.event_due = false; - Some(self.map_key(input.unwrap())) + Box::new(c) } - fn next_event(&mut self) -> Event { - self.request(); - - let input = self.input.recv().unwrap(); - self.event_due = false; - self.map_key(input) + fn apply_colors(&self, colors: theme::ColorPair) { + with_color(&colors.front, |c| self.write(tcolor::Fg(c))); + with_color(&colors.back, |c| self.write(tcolor::Bg(c))); } fn map_key(&mut self, event: TEvent) -> Event { @@ -184,68 +177,33 @@ impl InputParser { _ => Event::Unknown(vec![]), } } -} -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 { - /// Creates a new termion-based backend. - pub fn init() -> Box { - 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))); + fn write(&self, content: T) + where + T: std::fmt::Display, + { + write!(self.terminal.borrow_mut(), "{}", content).unwrap(); } } impl backend::Backend for Backend { fn finish(&mut self) { - print!("{}{}", termion::cursor::Show, termion::cursor::Goto(1, 1)); - print!( + write!( + self.terminal.get_mut(), + "{}{}", + termion::cursor::Show, + termion::cursor::Goto(1, 1) + ) + .unwrap(); + + write!( + self.terminal.get_mut(), "{}[49m{}[39m{}", 27 as char, 27 as char, termion::clear::All - ); + ) + .unwrap(); } 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) { - 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) { - 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 { @@ -273,6 +243,8 @@ impl backend::Backend for Backend { } 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)); (x, y).into() } @@ -282,52 +254,31 @@ impl backend::Backend for Backend { front: color, back: color, }); - print!("{}", termion::clear::All); + + self.write(termion::clear::All); } fn refresh(&mut self) { - self.terminal.flush().unwrap(); + self.terminal.get_mut().flush().unwrap(); } 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), text - ); + ) + .unwrap(); } - fn start_input_thread( - &mut self, event_sink: Sender>, - input_request: Receiver, - ) { - let running = Arc::new(AtomicBool::new(true)); - - #[cfg(unix)] - { - 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); - }); + fn poll_event(&mut self) -> Option { + let event = select! { + recv(self.input_receiver) -> event => event.ok(), + recv(self.resize_receiver) -> _ => return Some(Event::WindowResize), + default => return None, + }; + event.map(|event| self.map_key(event)) } } diff --git a/src/cursive.rs b/src/cursive.rs index a5e7d41..0fd476d 100644 --- a/src/cursive.rs +++ b/src/cursive.rs @@ -33,7 +33,7 @@ pub struct Cursive { // If it changed, clear the screen. last_sizes: Vec, - fps: u32, + autorefresh: bool, active_screen: ScreenId, @@ -43,27 +43,14 @@ pub struct Cursive { cb_source: Receiver>, cb_sink: Sender>, - - event_source: Receiver>, - - // Sends true or false after each event. - input_trigger: Sender, - 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), - /// A timeout ran out - Timeout, } /// Identifies a screen in the cursive root. pub type ScreenId = usize; +/// Convenient alias to the result of `Cursive::cb_sink`. +pub type CbSink = Sender>; + /// Asynchronous callback function trait. /// /// Every `FnOnce(&mut Cursive) -> () + Send` automatically @@ -145,15 +132,11 @@ impl Cursive { let theme = theme::load_default(); 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 mut backend = backend_init(); - backend.start_input_thread(event_sink, input_source); + let backend = backend_init(); Cursive { - fps: 0, + autorefresh: false, theme, screens: vec![views::StackView::new()], last_sizes: Vec::new(), @@ -163,10 +146,7 @@ impl Cursive { running: true, cb_source, cb_sink, - event_source, backend, - input_trigger: input_sink, - expecting_event: false, } } @@ -240,9 +220,6 @@ impl Cursive { /// Callbacks will be executed in the order /// 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 /// /// ```rust @@ -250,15 +227,12 @@ impl Cursive { /// # use cursive::*; /// # fn main() { /// let mut siv = Cursive::dummy(); - /// siv.set_fps(10); /// /// // quit() will be called during the next event cycle /// siv.cb_sink().send(Box::new(|s: &mut Cursive| s.quit())).unwrap(); /// # } /// ``` - /// - /// [`set_fps`]: #method.set_fps - pub fn cb_sink(&self) -> &Sender> { + pub fn cb_sink(&self) -> &CbSink { &self.cb_sink } @@ -364,19 +338,11 @@ impl Cursive { 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. - /// - /// You currently need this to regularly check - /// 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; + /// When on, regularly redraws everything, even when no input is given. + pub fn set_autorefresh(&mut self, autorefresh: bool) { + self.autorefresh = autorefresh; } /// Returns a reference to the currently active screen. @@ -649,63 +615,6 @@ impl Cursive { self.screen_mut().reposition_layer(layer, position); } - fn peek(&mut self) -> Option { - // 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 { - 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 fn on_ignored_event(&mut self, event: Event) { let cb_list = match self.global_callbacks.get(&event) { @@ -830,6 +739,8 @@ impl Cursive { pub fn run(&mut self) { self.running = true; + self.refresh(); + // And the big event loop begins! while self.running { self.step(); @@ -843,6 +754,41 @@ impl Cursive { /// /// [`run(&mut self)`]: #method.run 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? // Probably, actually. // TODO: Do we need to re-layout everytime? @@ -852,40 +798,17 @@ impl Cursive { // (Is this getting repetitive? :p) self.draw(); 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. pub fn quit(&mut self) { self.running = false; } + + /// Does not do anything. + pub fn noop(&mut self) { + // foo + } } impl Drop for Cursive { diff --git a/src/lib.rs b/src/lib.rs index aa05d81..15a0ca8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -129,7 +129,7 @@ mod utf8; pub mod backend; -pub use cursive::{CbFunc, Cursive, ScreenId}; +pub use cursive::{CbFunc, CbSink, Cursive, ScreenId}; pub use printer::Printer; pub use vec::Vec2; pub use with::With;