From 05e1212a50e0dfe709af7616f3033006c1a5bdcc Mon Sep 17 00:00:00 2001 From: Alexandre Bury Date: Sun, 20 May 2018 09:59:35 -0700 Subject: [PATCH 1/5] Refactor Backend input model Backends now have to send input to the given `chan::Sender`. They send these events from a separate thread, allowing selection between input and callbacks. This currently breaks the BearLibTerminal backend, which requires all calls to come from the UI thread. This might not be super-safe for the ncurses backend also. We hope that input and output are separate enough that they can run concurrently without problem. --- Cargo.toml | 7 +- examples/position.rs | 1 - examples/progress.rs | 6 +- examples/vpv.rs | 4 +- src/backend/blt.rs | 178 ++++++++------ src/backend/curses/n.rs | 247 ++++++++++--------- src/backend/curses/pan.rs | 498 ++++++++++++++++++++------------------ src/backend/dummy.rs | 10 +- src/backend/mod.rs | 13 +- src/backend/termion.rs | 165 +++++++------ src/cursive.rs | 225 +++++++++++------ src/lib.rs | 6 +- 12 files changed, 746 insertions(+), 614 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e098758..b5180e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ unicode-segmentation = "1.0" unicode-width = "0.1" xi-unicode = "0.1.0" libc = "0.2" +chan = "0.1" [dependencies.maplit] optional = true @@ -33,10 +34,6 @@ version = "1.0.0" optional = true version = "1.3.1" -[dependencies.chan] -optional = true -version = "0.1.18" - [dependencies.chan-signal] optional = true version = "0.3" @@ -70,7 +67,7 @@ default = ["ncurses-backend"] markdown = ["pulldown-cmark"] ncurses-backend = ["ncurses", "maplit"] pancurses-backend = ["pancurses", "maplit"] -termion-backend = ["termion", "chan", "chan-signal"] +termion-backend = ["termion", "chan-signal"] [lib] name = "cursive" diff --git a/examples/position.rs b/examples/position.rs index cf4dff4..2e27ee9 100644 --- a/examples/position.rs +++ b/examples/position.rs @@ -23,7 +23,6 @@ fn move_top(c: &mut Cursive, x_in: isize, y_in: isize) { fn main() { let mut siv = Cursive::default(); - siv.set_fps(60); // We can quit by pressing `q` siv.add_global_callback('q', Cursive::quit); diff --git a/examples/progress.rs b/examples/progress.rs index 107b036..4ec110f 100644 --- a/examples/progress.rs +++ b/examples/progress.rs @@ -27,7 +27,7 @@ fn main() { .content(Button::new("Start", phase_1)), ); - // Auto-refresh is currently required for animated views + // Auto-refresh is required for animated views siv.set_fps(30); siv.run(); @@ -52,7 +52,7 @@ fn phase_1(s: &mut Cursive) { fake_load(n_max, &counter); // When we're done, send a callback through the channel - cb.send(Box::new(coffee_break)).unwrap(); + cb.send(Box::new(coffee_break)); }) .full_width(), )); @@ -110,7 +110,7 @@ fn phase_2(s: &mut Cursive) { } } - cb.send(Box::new(final_step)).unwrap(); + cb.send(Box::new(final_step)); }); } diff --git a/examples/vpv.rs b/examples/vpv.rs index c3ba833..5512f4c 100644 --- a/examples/vpv.rs +++ b/examples/vpv.rs @@ -68,9 +68,7 @@ fn main() { } // When we're done, shut down the application - cb_sink - .send(Box::new(|s: &mut Cursive| s.quit())) - .unwrap(); + cb_sink.send(Box::new(|s: &mut Cursive| s.quit())); }); // Add a single view: progress status diff --git a/src/backend/blt.rs b/src/backend/blt.rs index fcc9997..91a7922 100644 --- a/src/backend/blt.rs +++ b/src/backend/blt.rs @@ -14,6 +14,8 @@ use event::{Event, Key, MouseButton, MouseEvent}; use std::collections::HashSet; use theme::{BaseColor, Color, ColorPair, Effect}; use vec::Vec2; +use chan; +use std::thread; enum ColorRole { Foreground, @@ -21,31 +23,81 @@ enum ColorRole { } pub struct Backend { - mouse_position: Vec2, - buttons_pressed: HashSet, } -impl Backend { - pub fn init() -> Box { - terminal::open("Cursive", 80, 24); - terminal::set(terminal::config::Window::empty().resizeable(true)); - terminal::set(vec![ - terminal::config::InputFilter::Group { - group: terminal::config::InputFilterGroup::Keyboard, - both: false, - }, - terminal::config::InputFilter::Group { - group: terminal::config::InputFilterGroup::Mouse, - both: true, - }, - ]); +struct InputParser { + event_sink: chan::Sender, + buttons_pressed: HashSet, + mouse_position: Vec2, +} - let c = Backend { - mouse_position: Vec2::zero(), +impl InputParser { + fn new(event_sink: chan::Sender) -> Self { + InputParser { + event_sink, buttons_pressed: HashSet::new(), - }; + mouse_position: Vec2::zero(), + } + } - Box::new(c) + fn parse_next(&mut self) { + + // TODO: we could add backend-specific controls here. + // Ex: ctrl+mouse wheel cause window cellsize to change + let event = if let Some(ev) = terminal::wait_event() { + match ev { + BltEvent::Close => Event::Exit, + BltEvent::Resize { .. } => Event::WindowResize, + // TODO: mouse support + BltEvent::MouseMove { x, y } => { + self.mouse_position = Vec2::new(x as usize, y as usize); + // TODO: find out if a button is pressed? + match self.buttons_pressed.iter().next() { + None => Event::Refresh, + Some(btn) => Event::Mouse { + event: MouseEvent::Hold(*btn), + position: self.mouse_position, + offset: Vec2::zero(), + }, + } + } + BltEvent::MouseScroll { delta } => Event::Mouse { + event: if delta < 0 { + MouseEvent::WheelUp + } else { + MouseEvent::WheelDown + }, + position: self.mouse_position, + offset: Vec2::zero(), + }, + BltEvent::KeyPressed { key, ctrl, shift } => { + self.blt_keycode_to_ev(key, shift, ctrl) + } + // TODO: there's no Key::Shift/Ctrl for w/e reason + BltEvent::ShiftPressed => Event::Refresh, + BltEvent::ControlPressed => Event::Refresh, + // TODO: what should we do here? + BltEvent::KeyReleased { key, .. } => { + // It's probably a mouse key. + blt_keycode_to_mouse_button(key) + .map(|btn| { + self.buttons_pressed.remove(&btn); + Event::Mouse { + event: MouseEvent::Release(btn), + position: self.mouse_position, + offset: Vec2::zero(), + } + }) + .unwrap_or(Event::Unknown(vec![])) + } + BltEvent::ShiftReleased | BltEvent::ControlReleased => { + Event::Refresh + } + } + } else { + Event::Refresh + }; + self.event_sink.send(event); } fn blt_keycode_to_ev( @@ -171,6 +223,28 @@ impl Backend { } } +impl Backend { + pub fn init() -> Box { + terminal::open("Cursive", 80, 24); + terminal::set(terminal::config::Window::empty().resizeable(true)); + terminal::set(vec![ + terminal::config::InputFilter::Group { + group: terminal::config::InputFilterGroup::Keyboard, + both: false, + }, + terminal::config::InputFilter::Group { + group: terminal::config::InputFilterGroup::Mouse, + both: true, + }, + ]); + + let c = Backend {}; + + Box::new(c) + } + +} + impl backend::Backend for Backend { fn finish(&mut self) { terminal::close(); @@ -246,66 +320,14 @@ impl backend::Backend for Backend { terminal::print_xy(pos.x as i32, pos.y as i32, text); } - fn set_refresh_rate(&mut self, _: u32) { - // TODO: unsupported - } + fn start_input_thread(&mut self, event_sink: chan::Sender) { + let mut parser = InputParser::new(event_sink); - fn poll_event(&mut self) -> Event { - // TODO: we could add backend-specific controls here. - // Ex: ctrl+mouse wheel cause window cellsize to change - if let Some(ev) = terminal::wait_event() { - match ev { - BltEvent::Close => Event::Exit, - BltEvent::Resize { .. } => Event::WindowResize, - // TODO: mouse support - BltEvent::MouseMove { x, y } => { - self.mouse_position = Vec2::new(x as usize, y as usize); - // TODO: find out if a button is pressed? - match self.buttons_pressed.iter().next() { - None => Event::Refresh, - Some(btn) => Event::Mouse { - event: MouseEvent::Hold(*btn), - position: self.mouse_position, - offset: Vec2::zero(), - }, - } - } - BltEvent::MouseScroll { delta } => Event::Mouse { - event: if delta < 0 { - MouseEvent::WheelUp - } else { - MouseEvent::WheelDown - }, - position: self.mouse_position, - offset: Vec2::zero(), - }, - BltEvent::KeyPressed { key, ctrl, shift } => { - self.blt_keycode_to_ev(key, shift, ctrl) - } - // TODO: there's no Key::Shift/Ctrl for w/e reason - BltEvent::ShiftPressed => Event::Refresh, - BltEvent::ControlPressed => Event::Refresh, - // TODO: what should we do here? - BltEvent::KeyReleased { key, .. } => { - // It's probably a mouse key. - blt_keycode_to_mouse_button(key) - .map(|btn| { - self.buttons_pressed.remove(&btn); - Event::Mouse { - event: MouseEvent::Release(btn), - position: self.mouse_position, - offset: Vec2::zero(), - } - }) - .unwrap_or(Event::Unknown(vec![])) - } - BltEvent::ShiftReleased | BltEvent::ControlReleased => { - Event::Refresh - } + thread::spawn(move || { + loop { + parser.parse_next(); } - } else { - Event::Refresh - } + }); } } diff --git a/src/backend/curses/n.rs b/src/backend/curses/n.rs index 6d38bff..da2908e 100644 --- a/src/backend/curses/n.rs +++ b/src/backend/curses/n.rs @@ -4,27 +4,140 @@ use self::ncurses::mmask_t; use self::super::split_i32; use backend; use event::{Event, Key, MouseButton, MouseEvent}; -use libc; +use theme::{Color, ColorPair, Effect}; +use utf8; +use vec::Vec2; + use std::cell::{Cell, RefCell}; use std::collections::HashMap; use std::ffi::CString; use std::fs::File; use std::io; use std::io::{Write}; -use theme::{Color, ColorPair, Effect}; -use utf8; -use vec::Vec2; +use std::thread; + +use libc; +use chan; pub struct Backend { current_style: Cell, // Maps (front, back) ncurses colors to ncurses pairs pairs: RefCell>, +} +struct InputParser { key_codes: HashMap, - last_mouse_button: Option, - event_queue: Vec, + event_sink: chan::Sender, +} + +impl InputParser { + fn new(event_sink: chan::Sender) -> Self { + InputParser { + key_codes: initialize_keymap(), + last_mouse_button: None, + event_sink, + } + } + + fn parse_next(&mut self) { + let ch: i32 = ncurses::getch(); + + // Is it a UTF-8 starting point? + let event = if 32 <= ch && ch <= 255 && ch != 127 { + utf8::read_char(ch as u8, || Some(ncurses::getch() as u8)) + .map(Event::Char) + .unwrap_or_else(|e| { + warn!("Error reading input: {}", e); + Event::Unknown(vec![ch as u8]) + }) + } else { + self.parse_ncurses_char(ch) + }; + self.event_sink.send(event); + } + + fn parse_ncurses_char(&mut self, ch: i32) -> Event { + // eprintln!("Found {:?}", ncurses::keyname(ch)); + if ch == ncurses::KEY_MOUSE { + self.parse_mouse_event() + } else { + self.key_codes + .get(&ch) + .cloned() + .unwrap_or_else(|| Event::Unknown(split_i32(ch))) + } + } + + fn parse_mouse_event(&mut self) -> Event { + let mut mevent = ncurses::MEVENT { + id: 0, + x: 0, + y: 0, + z: 0, + bstate: 0, + }; + if ncurses::getmouse(&mut mevent as *mut ncurses::MEVENT) + == ncurses::OK + { + // eprintln!("{:032b}", mevent.bstate); + // Currently unused + let _shift = + (mevent.bstate & ncurses::BUTTON_SHIFT as mmask_t) != 0; + let _alt = (mevent.bstate & ncurses::BUTTON_ALT as mmask_t) != 0; + let _ctrl = (mevent.bstate & ncurses::BUTTON_CTRL as mmask_t) != 0; + + mevent.bstate &= !(ncurses::BUTTON_SHIFT | ncurses::BUTTON_ALT + | ncurses::BUTTON_CTRL) + as mmask_t; + + let make_event = |event| Event::Mouse { + offset: Vec2::zero(), + position: Vec2::new(mevent.x as usize, mevent.y as usize), + event, + }; + + if mevent.bstate == ncurses::REPORT_MOUSE_POSITION as mmask_t { + // The event is either a mouse drag event, + // or a weird double-release event. :S + self.last_mouse_button + .map(MouseEvent::Hold) + .map(&make_event) + .unwrap_or_else(|| Event::Unknown(vec![])) + } else { + // Identify the button + let mut bare_event = mevent.bstate & ((1 << 25) - 1); + + let mut event = None; + while bare_event != 0 { + let single_event = 1 << bare_event.trailing_zeros(); + bare_event ^= single_event; + + // Process single_event + on_mouse_event(single_event as i32, |e| { + if event.is_none() { + event = Some(e); + } else { + self.event_sink.send(make_event(e)); + } + }); + } + if let Some(event) = event { + if let Some(btn) = event.button() { + self.last_mouse_button = Some(btn); + } + make_event(event) + } else { + debug!("No event parsed?..."); + Event::Unknown(vec![]) + } + } + } else { + debug!("Ncurses event not recognized."); + Event::Unknown(vec![]) + } + } } fn find_closest_pair(pair: &ColorPair) -> (i16, i16) { @@ -85,11 +198,6 @@ impl Backend { let c = Backend { current_style: Cell::new(ColorPair::from_256colors(0, 0)), pairs: RefCell::new(HashMap::new()), - - last_mouse_button: None, - event_queue: Vec::new(), - - key_codes: initialize_keymap(), }; Box::new(c) @@ -139,86 +247,7 @@ impl Backend { ncurses::attron(style); } - fn parse_mouse_event(&mut self) -> Event { - let mut mevent = ncurses::MEVENT { - id: 0, - x: 0, - y: 0, - z: 0, - bstate: 0, - }; - if ncurses::getmouse(&mut mevent as *mut ncurses::MEVENT) - == ncurses::OK - { - // eprintln!("{:032b}", mevent.bstate); - // Currently unused - let _shift = - (mevent.bstate & ncurses::BUTTON_SHIFT as mmask_t) != 0; - let _alt = (mevent.bstate & ncurses::BUTTON_ALT as mmask_t) != 0; - let _ctrl = (mevent.bstate & ncurses::BUTTON_CTRL as mmask_t) != 0; - mevent.bstate &= !(ncurses::BUTTON_SHIFT | ncurses::BUTTON_ALT - | ncurses::BUTTON_CTRL) - as mmask_t; - - let make_event = |event| Event::Mouse { - offset: Vec2::zero(), - position: Vec2::new(mevent.x as usize, mevent.y as usize), - event, - }; - - if mevent.bstate == ncurses::REPORT_MOUSE_POSITION as mmask_t { - // The event is either a mouse drag event, - // or a weird double-release event. :S - self.last_mouse_button - .map(MouseEvent::Hold) - .map(&make_event) - .unwrap_or_else(|| Event::Unknown(vec![])) - } else { - // Identify the button - let mut bare_event = mevent.bstate & ((1 << 25) - 1); - - let mut event = None; - while bare_event != 0 { - let single_event = 1 << bare_event.trailing_zeros(); - bare_event ^= single_event; - - // Process single_event - on_mouse_event(single_event as i32, |e| { - if event.is_none() { - event = Some(e); - } else { - self.event_queue.push(make_event(e)); - } - }); - } - if let Some(event) = event { - if let Some(btn) = event.button() { - self.last_mouse_button = Some(btn); - } - make_event(event) - } else { - debug!("No event parsed?..."); - Event::Unknown(vec![]) - } - } - } else { - debug!("Ncurses event not recognized."); - Event::Unknown(vec![]) - } - } - - fn parse_ncurses_char(&mut self, ch: i32) -> Event { - // eprintln!("Found {:?}", ncurses::keyname(ch)); - if ch == ncurses::KEY_MOUSE { - self.parse_mouse_event() - } else { - self.key_codes - .get(&ch) - .cloned() - .unwrap_or_else(|| Event::Unknown(split_i32(ch))) - } - } } impl backend::Backend for Backend { @@ -233,6 +262,18 @@ impl backend::Backend for Backend { ncurses::has_colors() } + fn start_input_thread(&mut self, event_sink: chan::Sender) { + let mut parser = InputParser::new(event_sink); + + // Start an input thread + thread::spawn(move || { + // TODO: use an atomic boolean to stop the thread on finish + loop { + parser.parse_next(); + } + }); + } + fn finish(&mut self) { write_to_tty(b"\x1B[?1002l").unwrap(); ncurses::endwin(); @@ -287,32 +328,6 @@ impl backend::Backend for Backend { fn print_at(&self, pos: Vec2, text: &str) { ncurses::mvaddstr(pos.y as i32, pos.x as i32, text); } - - fn poll_event(&mut self) -> Event { - self.event_queue.pop().unwrap_or_else(|| { - let ch: i32 = ncurses::getch(); - - // Is it a UTF-8 starting point? - if 32 <= ch && ch <= 255 && ch != 127 { - utf8::read_char(ch as u8, || Some(ncurses::getch() as u8)) - .map(Event::Char) - .unwrap_or_else(|e| { - warn!("Error reading input: {}", e); - Event::Unknown(vec![ch as u8]) - }) - } else { - self.parse_ncurses_char(ch) - } - }) - } - - fn set_refresh_rate(&mut self, fps: u32) { - if fps == 0 { - ncurses::timeout(-1); - } else { - ncurses::timeout(1000 / fps as i32); - } - } } /// Returns the Key enum corresponding to the given ncurses event. diff --git a/src/backend/curses/pan.rs b/src/backend/curses/pan.rs index 0f350dd..98b2495 100644 --- a/src/backend/curses/pan.rs +++ b/src/backend/curses/pan.rs @@ -9,19 +9,264 @@ use std::collections::HashMap; use std::io::{stdout, Write}; use theme::{Color, ColorPair, Effect}; use vec::Vec2; +use std::sync::Arc; +use chan; +use std::thread; pub struct Backend { // Used current_style: Cell, pairs: RefCell>, - key_codes: HashMap, - - last_mouse_button: Option, - event_queue: Vec, - // pancurses needs a handle to the current window. - window: pancurses::Window, + window: Arc, +} + +struct InputParser { + key_codes: HashMap, + last_mouse_button: Option, + event_sink: chan::Sender, + 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 {} + +impl InputParser { + fn new(event_sink: chan::Sender, window: Arc) -> Self { + InputParser { + key_codes: initialize_keymap(), + last_mouse_button: None, + event_sink, + window, + } + } + + fn parse_next(&mut self) { + let event = if let Some(ev) = self.window.getch() { + match ev { + pancurses::Input::Character('\n') => { + Event::Key(Key::Enter) + } + // TODO: wait for a very short delay. If more keys are + // pipelined, it may be an escape sequence. + pancurses::Input::Character('\u{7f}') + | pancurses::Input::Character('\u{8}') => { + Event::Key(Key::Backspace) + } + pancurses::Input::Character('\u{9}') => { + Event::Key(Key::Tab) + } + pancurses::Input::Character('\u{1b}') => { + Event::Key(Key::Esc) + } + pancurses::Input::Character(c) if (c as u32) <= 26 => { + Event::CtrlChar((b'a' - 1 + c as u8) as char) + } + pancurses::Input::Character(c) => Event::Char(c), + // TODO: Some key combos are not recognized by pancurses, + // but are sent as Unknown. We could still parse them here. + pancurses::Input::Unknown(code) => self.key_codes + // pancurses does some weird keycode mapping + .get(&(code + 256 + 48)) + .cloned() + .unwrap_or_else(|| { + warn!("Unknown: {}", code); + Event::Unknown(split_i32(code)) + }), + // TODO: I honestly have no fucking idea what KeyCodeYes is + pancurses::Input::KeyCodeYes => Event::Refresh, + pancurses::Input::KeyBreak => Event::Key(Key::PauseBreak), + pancurses::Input::KeyDown => Event::Key(Key::Down), + pancurses::Input::KeyUp => Event::Key(Key::Up), + pancurses::Input::KeyLeft => Event::Key(Key::Left), + pancurses::Input::KeyRight => Event::Key(Key::Right), + pancurses::Input::KeyHome => Event::Key(Key::Home), + pancurses::Input::KeyBackspace => { + Event::Key(Key::Backspace) + } + pancurses::Input::KeyF0 => Event::Key(Key::F0), + pancurses::Input::KeyF1 => Event::Key(Key::F1), + pancurses::Input::KeyF2 => Event::Key(Key::F2), + pancurses::Input::KeyF3 => Event::Key(Key::F3), + pancurses::Input::KeyF4 => Event::Key(Key::F4), + pancurses::Input::KeyF5 => Event::Key(Key::F5), + pancurses::Input::KeyF6 => Event::Key(Key::F6), + pancurses::Input::KeyF7 => Event::Key(Key::F7), + pancurses::Input::KeyF8 => Event::Key(Key::F8), + pancurses::Input::KeyF9 => Event::Key(Key::F9), + pancurses::Input::KeyF10 => Event::Key(Key::F10), + pancurses::Input::KeyF11 => Event::Key(Key::F11), + pancurses::Input::KeyF12 => Event::Key(Key::F12), + pancurses::Input::KeyF13 => Event::Shift(Key::F1), + pancurses::Input::KeyF14 => Event::Shift(Key::F2), + pancurses::Input::KeyF15 => Event::Shift(Key::F3), + pancurses::Input::KeyDL => Event::Refresh, + pancurses::Input::KeyIL => Event::Refresh, + pancurses::Input::KeyDC => Event::Key(Key::Del), + pancurses::Input::KeyIC => Event::Key(Key::Ins), + pancurses::Input::KeyEIC => Event::Refresh, + pancurses::Input::KeyClear => Event::Refresh, + pancurses::Input::KeyEOS => Event::Refresh, + pancurses::Input::KeyEOL => Event::Refresh, + pancurses::Input::KeySF => Event::Shift(Key::Down), + pancurses::Input::KeySR => Event::Shift(Key::Up), + pancurses::Input::KeyNPage => Event::Key(Key::PageDown), + pancurses::Input::KeyPPage => Event::Key(Key::PageUp), + pancurses::Input::KeySTab => Event::Shift(Key::Tab), + pancurses::Input::KeyCTab => Event::Ctrl(Key::Tab), + pancurses::Input::KeyCATab => Event::CtrlAlt(Key::Tab), + pancurses::Input::KeyEnter => Event::Key(Key::Enter), + pancurses::Input::KeySReset => Event::Refresh, + pancurses::Input::KeyReset => Event::Refresh, + pancurses::Input::KeyPrint => Event::Refresh, + pancurses::Input::KeyLL => Event::Refresh, + pancurses::Input::KeyAbort => Event::Refresh, + pancurses::Input::KeySHelp => Event::Refresh, + pancurses::Input::KeyLHelp => Event::Refresh, + pancurses::Input::KeyBTab => Event::Shift(Key::Tab), + pancurses::Input::KeyBeg => Event::Refresh, + pancurses::Input::KeyCancel => Event::Refresh, + pancurses::Input::KeyClose => Event::Refresh, + pancurses::Input::KeyCommand => Event::Refresh, + pancurses::Input::KeyCopy => Event::Refresh, + pancurses::Input::KeyCreate => Event::Refresh, + pancurses::Input::KeyEnd => Event::Key(Key::End), + pancurses::Input::KeyExit => Event::Refresh, + pancurses::Input::KeyFind => Event::Refresh, + pancurses::Input::KeyHelp => Event::Refresh, + pancurses::Input::KeyMark => Event::Refresh, + pancurses::Input::KeyMessage => Event::Refresh, + pancurses::Input::KeyMove => Event::Refresh, + pancurses::Input::KeyNext => Event::Refresh, + pancurses::Input::KeyOpen => Event::Refresh, + pancurses::Input::KeyOptions => Event::Refresh, + pancurses::Input::KeyPrevious => Event::Refresh, + pancurses::Input::KeyRedo => Event::Refresh, + pancurses::Input::KeyReference => Event::Refresh, + pancurses::Input::KeyRefresh => Event::Refresh, + pancurses::Input::KeyReplace => Event::Refresh, + pancurses::Input::KeyRestart => Event::Refresh, + pancurses::Input::KeyResume => Event::Refresh, + pancurses::Input::KeySave => Event::Refresh, + pancurses::Input::KeySBeg => Event::Refresh, + pancurses::Input::KeySCancel => Event::Refresh, + pancurses::Input::KeySCommand => Event::Refresh, + pancurses::Input::KeySCopy => Event::Refresh, + pancurses::Input::KeySCreate => Event::Refresh, + pancurses::Input::KeySDC => Event::Shift(Key::Del), + pancurses::Input::KeySDL => Event::Refresh, + pancurses::Input::KeySelect => Event::Refresh, + pancurses::Input::KeySEnd => Event::Shift(Key::End), + pancurses::Input::KeySEOL => Event::Refresh, + pancurses::Input::KeySExit => Event::Refresh, + pancurses::Input::KeySFind => Event::Refresh, + pancurses::Input::KeySHome => Event::Shift(Key::Home), + pancurses::Input::KeySIC => Event::Shift(Key::Ins), + pancurses::Input::KeySLeft => Event::Shift(Key::Left), + pancurses::Input::KeySMessage => Event::Refresh, + pancurses::Input::KeySMove => Event::Refresh, + pancurses::Input::KeySNext => Event::Shift(Key::PageDown), + pancurses::Input::KeySOptions => Event::Refresh, + pancurses::Input::KeySPrevious => { + Event::Shift(Key::PageUp) + } + pancurses::Input::KeySPrint => Event::Refresh, + pancurses::Input::KeySRedo => Event::Refresh, + pancurses::Input::KeySReplace => Event::Refresh, + pancurses::Input::KeySRight => Event::Shift(Key::Right), + pancurses::Input::KeySResume => Event::Refresh, + pancurses::Input::KeySSave => Event::Refresh, + pancurses::Input::KeySSuspend => Event::Refresh, + pancurses::Input::KeySUndo => Event::Refresh, + pancurses::Input::KeySuspend => Event::Refresh, + pancurses::Input::KeyUndo => Event::Refresh, + pancurses::Input::KeyResize => { + // Let pancurses adjust their structures when the + // window is resized. + // Do it for Windows only, as 'resize_term' is not + // implemented for Unix + if cfg!(target_os = "windows") { + pancurses::resize_term(0, 0); + } + Event::WindowResize + } + pancurses::Input::KeyEvent => Event::Refresh, + // TODO: mouse support + pancurses::Input::KeyMouse => self.parse_mouse_event(), + pancurses::Input::KeyA1 => Event::Refresh, + pancurses::Input::KeyA3 => Event::Refresh, + pancurses::Input::KeyB2 => Event::Key(Key::NumpadCenter), + pancurses::Input::KeyC1 => Event::Refresh, + pancurses::Input::KeyC3 => Event::Refresh, + } + } else { + Event::Refresh + }; + self.event_sink.send(event); + } + + fn parse_mouse_event(&mut self) -> Event { + let mut mevent = match pancurses::getmouse() { + Err(code) => return Event::Unknown(split_i32(code)), + Ok(event) => event, + }; + + let _shift = (mevent.bstate & pancurses::BUTTON_SHIFT as mmask_t) != 0; + let _alt = (mevent.bstate & pancurses::BUTTON_ALT as mmask_t) != 0; + let _ctrl = (mevent.bstate & pancurses::BUTTON_CTRL as mmask_t) != 0; + + mevent.bstate &= !(pancurses::BUTTON_SHIFT | pancurses::BUTTON_ALT + | pancurses::BUTTON_CTRL) as mmask_t; + + let make_event = |event| Event::Mouse { + offset: Vec2::zero(), + position: Vec2::new(mevent.x as usize, mevent.y as usize), + event: event, + }; + + if mevent.bstate == pancurses::REPORT_MOUSE_POSITION as mmask_t { + // The event is either a mouse drag event, + // or a weird double-release event. :S + self.last_mouse_button + .map(MouseEvent::Hold) + .map(&make_event) + .unwrap_or_else(|| { + debug!("We got a mouse drag, but no last mouse pressed?"); + Event::Unknown(vec![]) + }) + } else { + // Identify the button + let mut bare_event = mevent.bstate & ((1 << 25) - 1); + + let mut event = None; + while bare_event != 0 { + let single_event = 1 << bare_event.trailing_zeros(); + bare_event ^= single_event; + + // Process single_event + on_mouse_event(single_event, |e| { + if event.is_none() { + event = Some(e); + } else { + self.event_sink.send(make_event(e)); + } + }); + } + if let Some(event) = event { + if let Some(btn) = event.button() { + self.last_mouse_button = Some(btn); + } + make_event(event) + } else { + debug!("No event parsed?..."); + Event::Unknown(vec![]) + } + } + } } fn find_closest_pair(pair: &ColorPair) -> (i16, i16) { @@ -54,10 +299,7 @@ impl Backend { let c = Backend { current_style: Cell::new(ColorPair::from_256colors(0, 0)), pairs: RefCell::new(HashMap::new()), - window: window, - last_mouse_button: None, - event_queue: Vec::new(), - key_codes: initialize_keymap(), + window: Arc::new(window), }; Box::new(c) @@ -109,64 +351,6 @@ impl Backend { self.window.attron(style); } - fn parse_mouse_event(&mut self) -> Event { - let mut mevent = match pancurses::getmouse() { - Err(code) => return Event::Unknown(split_i32(code)), - Ok(event) => event, - }; - - let _shift = (mevent.bstate & pancurses::BUTTON_SHIFT as mmask_t) != 0; - let _alt = (mevent.bstate & pancurses::BUTTON_ALT as mmask_t) != 0; - let _ctrl = (mevent.bstate & pancurses::BUTTON_CTRL as mmask_t) != 0; - - mevent.bstate &= !(pancurses::BUTTON_SHIFT | pancurses::BUTTON_ALT - | pancurses::BUTTON_CTRL) as mmask_t; - - let make_event = |event| Event::Mouse { - offset: Vec2::zero(), - position: Vec2::new(mevent.x as usize, mevent.y as usize), - event: event, - }; - - if mevent.bstate == pancurses::REPORT_MOUSE_POSITION as mmask_t { - // The event is either a mouse drag event, - // or a weird double-release event. :S - self.last_mouse_button - .map(MouseEvent::Hold) - .map(&make_event) - .unwrap_or_else(|| { - debug!("We got a mouse drag, but no last mouse pressed?"); - Event::Unknown(vec![]) - }) - } else { - // Identify the button - let mut bare_event = mevent.bstate & ((1 << 25) - 1); - - let mut event = None; - while bare_event != 0 { - let single_event = 1 << bare_event.trailing_zeros(); - bare_event ^= single_event; - - // Process single_event - on_mouse_event(single_event, |e| { - if event.is_none() { - event = Some(e); - } else { - self.event_queue.push(make_event(e)); - } - }); - } - if let Some(event) = event { - if let Some(btn) = event.button() { - self.last_mouse_button = Some(btn); - } - make_event(event) - } else { - debug!("No event parsed?..."); - Event::Unknown(vec![]) - } - } - } } impl backend::Backend for Backend { @@ -235,177 +419,15 @@ impl backend::Backend for Backend { self.window.mvaddstr(pos.y as i32, pos.x as i32, text); } - fn poll_event(&mut self) -> Event { - self.event_queue.pop().unwrap_or_else(|| { - if let Some(ev) = self.window.getch() { - match ev { - pancurses::Input::Character('\n') => { - Event::Key(Key::Enter) - } - // TODO: wait for a very short delay. If more keys are - // pipelined, it may be an escape sequence. - pancurses::Input::Character('\u{7f}') - | pancurses::Input::Character('\u{8}') => { - Event::Key(Key::Backspace) - } - pancurses::Input::Character('\u{9}') => { - Event::Key(Key::Tab) - } - pancurses::Input::Character('\u{1b}') => { - Event::Key(Key::Esc) - } - pancurses::Input::Character(c) if (c as u32) <= 26 => { - Event::CtrlChar((b'a' - 1 + c as u8) as char) - } - pancurses::Input::Character(c) => Event::Char(c), - // TODO: Some key combos are not recognized by pancurses, - // but are sent as Unknown. We could still parse them here. - pancurses::Input::Unknown(code) => self.key_codes - // pancurses does some weird keycode mapping - .get(&(code + 256 + 48)) - .cloned() - .unwrap_or_else(|| { - warn!("Unknown: {}", code); - Event::Unknown(split_i32(code)) - }), - // TODO: I honestly have no fucking idea what KeyCodeYes is - pancurses::Input::KeyCodeYes => Event::Refresh, - pancurses::Input::KeyBreak => Event::Key(Key::PauseBreak), - pancurses::Input::KeyDown => Event::Key(Key::Down), - pancurses::Input::KeyUp => Event::Key(Key::Up), - pancurses::Input::KeyLeft => Event::Key(Key::Left), - pancurses::Input::KeyRight => Event::Key(Key::Right), - pancurses::Input::KeyHome => Event::Key(Key::Home), - pancurses::Input::KeyBackspace => { - Event::Key(Key::Backspace) - } - pancurses::Input::KeyF0 => Event::Key(Key::F0), - pancurses::Input::KeyF1 => Event::Key(Key::F1), - pancurses::Input::KeyF2 => Event::Key(Key::F2), - pancurses::Input::KeyF3 => Event::Key(Key::F3), - pancurses::Input::KeyF4 => Event::Key(Key::F4), - pancurses::Input::KeyF5 => Event::Key(Key::F5), - pancurses::Input::KeyF6 => Event::Key(Key::F6), - pancurses::Input::KeyF7 => Event::Key(Key::F7), - pancurses::Input::KeyF8 => Event::Key(Key::F8), - pancurses::Input::KeyF9 => Event::Key(Key::F9), - pancurses::Input::KeyF10 => Event::Key(Key::F10), - pancurses::Input::KeyF11 => Event::Key(Key::F11), - pancurses::Input::KeyF12 => Event::Key(Key::F12), - pancurses::Input::KeyF13 => Event::Shift(Key::F1), - pancurses::Input::KeyF14 => Event::Shift(Key::F2), - pancurses::Input::KeyF15 => Event::Shift(Key::F3), - pancurses::Input::KeyDL => Event::Refresh, - pancurses::Input::KeyIL => Event::Refresh, - pancurses::Input::KeyDC => Event::Key(Key::Del), - pancurses::Input::KeyIC => Event::Key(Key::Ins), - pancurses::Input::KeyEIC => Event::Refresh, - pancurses::Input::KeyClear => Event::Refresh, - pancurses::Input::KeyEOS => Event::Refresh, - pancurses::Input::KeyEOL => Event::Refresh, - pancurses::Input::KeySF => Event::Shift(Key::Down), - pancurses::Input::KeySR => Event::Shift(Key::Up), - pancurses::Input::KeyNPage => Event::Key(Key::PageDown), - pancurses::Input::KeyPPage => Event::Key(Key::PageUp), - pancurses::Input::KeySTab => Event::Shift(Key::Tab), - pancurses::Input::KeyCTab => Event::Ctrl(Key::Tab), - pancurses::Input::KeyCATab => Event::CtrlAlt(Key::Tab), - pancurses::Input::KeyEnter => Event::Key(Key::Enter), - pancurses::Input::KeySReset => Event::Refresh, - pancurses::Input::KeyReset => Event::Refresh, - pancurses::Input::KeyPrint => Event::Refresh, - pancurses::Input::KeyLL => Event::Refresh, - pancurses::Input::KeyAbort => Event::Refresh, - pancurses::Input::KeySHelp => Event::Refresh, - pancurses::Input::KeyLHelp => Event::Refresh, - pancurses::Input::KeyBTab => Event::Shift(Key::Tab), - pancurses::Input::KeyBeg => Event::Refresh, - pancurses::Input::KeyCancel => Event::Refresh, - pancurses::Input::KeyClose => Event::Refresh, - pancurses::Input::KeyCommand => Event::Refresh, - pancurses::Input::KeyCopy => Event::Refresh, - pancurses::Input::KeyCreate => Event::Refresh, - pancurses::Input::KeyEnd => Event::Key(Key::End), - pancurses::Input::KeyExit => Event::Refresh, - pancurses::Input::KeyFind => Event::Refresh, - pancurses::Input::KeyHelp => Event::Refresh, - pancurses::Input::KeyMark => Event::Refresh, - pancurses::Input::KeyMessage => Event::Refresh, - pancurses::Input::KeyMove => Event::Refresh, - pancurses::Input::KeyNext => Event::Refresh, - pancurses::Input::KeyOpen => Event::Refresh, - pancurses::Input::KeyOptions => Event::Refresh, - pancurses::Input::KeyPrevious => Event::Refresh, - pancurses::Input::KeyRedo => Event::Refresh, - pancurses::Input::KeyReference => Event::Refresh, - pancurses::Input::KeyRefresh => Event::Refresh, - pancurses::Input::KeyReplace => Event::Refresh, - pancurses::Input::KeyRestart => Event::Refresh, - pancurses::Input::KeyResume => Event::Refresh, - pancurses::Input::KeySave => Event::Refresh, - pancurses::Input::KeySBeg => Event::Refresh, - pancurses::Input::KeySCancel => Event::Refresh, - pancurses::Input::KeySCommand => Event::Refresh, - pancurses::Input::KeySCopy => Event::Refresh, - pancurses::Input::KeySCreate => Event::Refresh, - pancurses::Input::KeySDC => Event::Shift(Key::Del), - pancurses::Input::KeySDL => Event::Refresh, - pancurses::Input::KeySelect => Event::Refresh, - pancurses::Input::KeySEnd => Event::Shift(Key::End), - pancurses::Input::KeySEOL => Event::Refresh, - pancurses::Input::KeySExit => Event::Refresh, - pancurses::Input::KeySFind => Event::Refresh, - pancurses::Input::KeySHome => Event::Shift(Key::Home), - pancurses::Input::KeySIC => Event::Shift(Key::Ins), - pancurses::Input::KeySLeft => Event::Shift(Key::Left), - pancurses::Input::KeySMessage => Event::Refresh, - pancurses::Input::KeySMove => Event::Refresh, - pancurses::Input::KeySNext => Event::Shift(Key::PageDown), - pancurses::Input::KeySOptions => Event::Refresh, - pancurses::Input::KeySPrevious => { - Event::Shift(Key::PageUp) - } - pancurses::Input::KeySPrint => Event::Refresh, - pancurses::Input::KeySRedo => Event::Refresh, - pancurses::Input::KeySReplace => Event::Refresh, - pancurses::Input::KeySRight => Event::Shift(Key::Right), - pancurses::Input::KeySResume => Event::Refresh, - pancurses::Input::KeySSave => Event::Refresh, - pancurses::Input::KeySSuspend => Event::Refresh, - pancurses::Input::KeySUndo => Event::Refresh, - pancurses::Input::KeySuspend => Event::Refresh, - pancurses::Input::KeyUndo => Event::Refresh, - pancurses::Input::KeyResize => { - // Let pancurses adjust their structures when the - // window is resized. - // Do it for Windows only, as 'resize_term' is not - // implemented for Unix - if cfg!(target_os = "windows") { - pancurses::resize_term(0, 0); - } - Event::WindowResize - } - pancurses::Input::KeyEvent => Event::Refresh, - // TODO: mouse support - pancurses::Input::KeyMouse => self.parse_mouse_event(), - pancurses::Input::KeyA1 => Event::Refresh, - pancurses::Input::KeyA3 => Event::Refresh, - pancurses::Input::KeyB2 => Event::Key(Key::NumpadCenter), - pancurses::Input::KeyC1 => Event::Refresh, - pancurses::Input::KeyC3 => Event::Refresh, - } - } else { - Event::Refresh - } - }) - } + fn start_input_thread(&mut self, event_sink: chan::Sender) { + let mut input_parser = InputParser::new(event_sink, Arc::clone(&self.window)); + + thread::spawn(move || { + loop { + input_parser.parse_next(); + } + }); - fn set_refresh_rate(&mut self, fps: u32) { - if fps == 0 { - self.window.timeout(-1); - } else { - self.window.timeout(1000 / fps as i32); - } } } diff --git a/src/backend/dummy.rs b/src/backend/dummy.rs index 0d7e907..d104d1e 100644 --- a/src/backend/dummy.rs +++ b/src/backend/dummy.rs @@ -4,6 +4,10 @@ use theme; use event; use vec::Vec2; +use std::thread; + +use chan; + pub struct Backend; impl Backend { @@ -28,16 +32,14 @@ impl backend::Backend for Backend { (1, 1).into() } - fn poll_event(&mut self) -> event::Event { - event::Event::Exit + fn start_input_thread(&mut self, event_sink: chan::Sender) { + thread::spawn(move || event_sink.send(event::Event::Exit)); } fn print_at(&self, _: Vec2, _: &str) {} fn clear(&self, _: theme::Color) {} - fn set_refresh_rate(&mut self, _: u32) {} - // This sets the Colours and returns the previous colours // to allow you to set them back when you're done. fn set_color(&self, colors: theme::ColorPair) -> theme::ColorPair { diff --git a/src/backend/mod.rs b/src/backend/mod.rs index e2a82bd..06b290d 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -10,6 +10,8 @@ use event; use theme; +use chan::Sender; + use vec::Vec2; pub mod dummy; @@ -27,6 +29,8 @@ pub trait Backend { /// This should clear any state in the terminal. fn finish(&mut self); + fn start_input_thread(&mut self, event_sink: Sender); + /// Refresh the screen. fn refresh(&mut self); @@ -36,8 +40,8 @@ pub trait Backend { /// Returns the screen size. fn screen_size(&self) -> Vec2; - /// Main input method - fn poll_event(&mut self) -> event::Event; + // /// Gets a receiver for input events. + // fn event_receiver(&mut self) -> Receiver; /// Main method used for printing fn print_at(&self, pos: Vec2, text: &str); @@ -45,11 +49,6 @@ pub trait Backend { /// Clears the screen with the given color. fn clear(&self, color: theme::Color); - /// Sets the refresh rate for the backend. - /// - /// If no event is detected in the interval, send an `Event::Refresh`. - fn set_refresh_rate(&mut self, fps: u32); - /// Starts using a new color. /// /// This should return the previously active color. diff --git a/src/backend/termion.rs b/src/backend/termion.rs index 1932898..7a5aa7e 100644 --- a/src/backend/termion.rs +++ b/src/backend/termion.rs @@ -28,52 +28,20 @@ use vec::Vec2; pub struct Backend { terminal: AlternateScreen>>, current_style: Cell, +} + +struct InputParser { input: chan::Receiver, resize: chan::Receiver, - timeout: Option, + event_sink: chan::Sender, last_button: Option, } -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 { - pub fn init() -> Box { - print!("{}", termion::cursor::Hide); - - let resize = chan_signal::notify(&[chan_signal::Signal::WINCH]); - - // TODO: lock stdout - let terminal = AlternateScreen::from(MouseTerminal::from( - ::std::io::stdout().into_raw_mode().unwrap(), - )); - +impl InputParser { + fn new(event_sink: chan::Sender) -> Self { let (sender, receiver) = chan::async(); + // Fill the input channel thread::spawn(move || { for key in ::std::io::stdin().events() { if let Ok(key) = key { @@ -82,22 +50,34 @@ impl Backend { } }); - let c = Backend { - terminal: terminal, - current_style: Cell::new(theme::ColorPair::from_256colors(0, 0)), - input: receiver, - resize: resize, - timeout: None, + InputParser { + resize: chan_signal::notify(&[chan_signal::Signal::WINCH]), + event_sink, last_button: None, - }; - - Box::new(c) + input: receiver, + } } - 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 next_event(&mut self) -> Event { + let result; + { + let input = &self.input; + let resize = &self.resize; + + chan_select!{ + resize.recv() => return Event::WindowResize, + input.recv() -> input => result = Some(input.unwrap()), + } + } + + self.map_key(result.unwrap()) } + + fn parse_next(&mut self) { + let event = self.next_event(); + self.event_sink.send(event); + } + fn map_key(&mut self, event: TEvent) -> Event { match event { TEvent::Unsupported(bytes) => Event::Unknown(bytes), @@ -173,6 +153,58 @@ impl Backend { } } +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 { + 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))); + } +} + impl backend::Backend for Backend { fn finish(&mut self) { print!("{}{}", termion::cursor::Show, termion::cursor::Goto(1, 1)); @@ -233,32 +265,13 @@ impl backend::Backend for Backend { ); } - fn set_refresh_rate(&mut self, fps: u32) { - self.timeout = Some(1000 / fps as u32); - } - - fn poll_event(&mut self) -> Event { - let result; - { - let input = &self.input; - let resize = &self.resize; - - if let Some(timeout) = self.timeout { - let timeout = chan::after_ms(timeout); - chan_select!{ - timeout.recv() => return Event::Refresh, - resize.recv() => return Event::WindowResize, - input.recv() -> input => result = Some(input.unwrap()), - } - } else { - chan_select!{ - resize.recv() => return Event::WindowResize, - input.recv() -> input => result = Some(input.unwrap()), - } + fn start_input_thread(&mut self, event_sink: chan::Sender) { + let mut parser = InputParser::new(event_sink); + thread::spawn(move || { + loop { + parser.parse_next(); } - } - - self.map_key(result.unwrap()) + }); } } diff --git a/src/cursive.rs b/src/cursive.rs index 784d0ac..335dcec 100644 --- a/src/cursive.rs +++ b/src/cursive.rs @@ -1,16 +1,57 @@ use backend; +use chan; use direction; use event::{Callback, Event, EventResult}; use printer::Printer; use std::any::Any; use std::collections::HashMap; use std::path::Path; -use std::sync::mpsc; use theme; use vec::Vec2; use view::{self, Finder, IntoBoxedView, Position, View}; use views::{self, LayerPosition}; +/// Central part of the cursive library. +/// +/// It initializes ncurses on creation and cleans up on drop. +/// To use it, you should populate it with views, layouts and callbacks, +/// then start the event loop with run(). +/// +/// It uses a list of screen, with one screen active at a time. +pub struct Cursive { + theme: theme::Theme, + screens: Vec, + global_callbacks: HashMap>, + menubar: views::Menubar, + + // Last layer sizes of the stack view. + // If it changed, clear the screen. + last_sizes: Vec, + + fps: u32, + + active_screen: ScreenId, + + running: bool, + + backend: Box, + + cb_source: chan::Receiver>, + cb_sink: chan::Sender>, + + event_source: chan::Receiver, +} + +/// 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; @@ -46,57 +87,45 @@ impl Default for Cursive { } } -#[cfg(all(not(feature = "termion"), not(feature = "pancurses"), feature = "bear-lib-terminal"))] +#[cfg( + all( + not(feature = "termion"), + not(feature = "pancurses"), + feature = "bear-lib-terminal" + ) +)] impl Default for Cursive { fn default() -> Self { Self::blt() } } -#[cfg(all(not(feature = "termion"), not(feature = "pancurses"), not(feature = "bear-lib-terminal"), feature = "ncurses"))] +#[cfg( + all( + not(feature = "termion"), + not(feature = "pancurses"), + not(feature = "bear-lib-terminal"), + feature = "ncurses" + ) +)] impl Default for Cursive { fn default() -> Self { Self::ncurses() } } -/// Central part of the cursive library. -/// -/// It initializes ncurses on creation and cleans up on drop. -/// To use it, you should populate it with views, layouts and callbacks, -/// then start the event loop with run(). -/// -/// It uses a list of screen, with one screen active at a time. -pub struct Cursive { - theme: theme::Theme, - screens: Vec, - global_callbacks: HashMap>, - menubar: views::Menubar, - - // Last layer sizes of the stack view. - // If it changed, clear the screen. - last_sizes: Vec, - - active_screen: ScreenId, - - running: bool, - - backend: Box, - - cb_source: mpsc::Receiver>, - cb_sink: mpsc::Sender>, -} - impl Cursive { /// Creates a new Cursive root, and initialize the back-end. - pub fn new(backend: Box) -> Self { + pub fn new(mut backend: Box) -> Self { let theme = theme::load_default(); - // theme.activate(&mut backend); - // let theme = theme::load_theme("assets/style.toml").unwrap(); - let (tx, rx) = mpsc::channel(); + let (cb_sink, cb_source) = chan::async(); + let (event_sink, event_source) = chan::async(); + + backend.start_input_thread(event_sink); Cursive { + fps: 0, theme: theme, screens: vec![views::StackView::new()], last_sizes: Vec::new(), @@ -104,8 +133,9 @@ impl Cursive { menubar: views::Menubar::new(), active_screen: 0, running: true, - cb_source: rx, - cb_sink: tx, + cb_source, + cb_sink, + event_source, backend: backend, } } @@ -167,7 +197,7 @@ impl Cursive { /// ``` /// /// [`set_fps`]: #method.set_fps - pub fn cb_sink(&self) -> &mpsc::Sender> { + pub fn cb_sink(&self) -> &chan::Sender> { &self.cb_sink } @@ -261,7 +291,7 @@ impl Cursive { /// /// `filename` must point to a valid toml file. pub fn load_theme_file>( - &mut self, filename: P + &mut self, filename: P, ) -> Result<(), theme::Error> { self.set_theme(try!(theme::load_theme_file(filename))); Ok(()) @@ -286,7 +316,8 @@ impl Cursive { /// /// [`cb_sink`]: #method.cb_sink pub fn set_fps(&mut self, fps: u32) { - self.backend.set_refresh_rate(fps) + // self.backend.set_refresh_rate(fps) + self.fps = fps; } /// Returns a reference to the currently active screen. @@ -366,7 +397,7 @@ impl Cursive { /// # } /// ``` pub fn call_on( - &mut self, sel: &view::Selector, callback: F + &mut self, sel: &view::Selector, callback: F, ) -> Option where V: View + Any, @@ -532,11 +563,42 @@ impl Cursive { /// Convenient stub forwarding layer repositioning. pub fn reposition_layer( - &mut self, layer: LayerPosition, position: Position + &mut self, layer: LayerPosition, position: Position, ) { self.screen_mut().reposition_layer(layer, position); } + // Wait until something happens. + fn poll(&mut self) -> Interruption { + let input_channel = &self.event_source; + let cb_channel = &self.cb_source; + + if self.fps > 0 { + let timeout = 1000 / self.fps; + let timeout = chan::after_ms(timeout); + chan_select! { + input_channel.recv() -> input => { + return Interruption::Event(input.unwrap()) + }, + cb_channel.recv() -> cb => { + return Interruption::Callback(cb.unwrap()) + }, + timeout.recv() => { + return Interruption::Timeout; + }, + } + } else { + chan_select! { + input_channel.recv() -> input => { + return Interruption::Event(input.unwrap()) + }, + cb_channel.recv() -> cb => { + return Interruption::Callback(cb.unwrap()) + }, + } + } + } + // Handles a key event when it was ignored by the current view fn on_event(&mut self, event: Event) { let cb_list = match self.global_callbacks.get(&event) { @@ -632,10 +694,6 @@ impl Cursive { /// /// [`run(&mut self)`]: #method.run pub fn step(&mut self) { - while let Ok(cb) = self.cb_source.try_recv() { - cb.call_box(self); - } - // Do we need to redraw everytime? // Probably, actually. // TODO: Do we need to re-layout everytime? @@ -647,43 +705,52 @@ impl Cursive { self.backend.refresh(); // Wait for next event. - // (If set_fps was called, this returns -1 now and then) - let event = self.backend.poll_event(); - if event == Event::Exit { - self.quit(); - } + match self.poll() { + Interruption::Event(event) => { + if event == Event::Exit { + self.quit(); + } - if event == Event::WindowResize { - self.clear(); - } + if event == Event::WindowResize { + self.clear(); + } - if let Event::Mouse { - event, position, .. - } = event - { - if event.grabs_focus() && !self.menubar.autohide - && !self.menubar.has_submenu() - && position.y == 0 - { - self.select_menubar(); - } - } + if let Event::Mouse { + event, position, .. + } = event + { + if event.grabs_focus() && !self.menubar.autohide + && !self.menubar.has_submenu() + && position.y == 0 + { + self.select_menubar(); + } + } - // Event dispatch order: - // * Focused element: - // * Menubar (if active) - // * Current screen (top layer) - // * Global callbacks - if self.menubar.receive_events() { - self.menubar.on_event(event).process(self); - } else { - let offset = if self.menubar.autohide { 0 } else { 1 }; - match self.screen_mut().on_event(event.relativized((0, offset))) { - // If the event was ignored, - // it is our turn to play with it. - EventResult::Ignored => self.on_event(event), - EventResult::Consumed(None) => (), - EventResult::Consumed(Some(cb)) => cb(self), + // Event dispatch order: + // * Focused element: + // * Menubar (if active) + // * Current screen (top layer) + // * Global callbacks + if self.menubar.receive_events() { + self.menubar.on_event(event).process(self); + } else { + let offset = if self.menubar.autohide { 0 } else { 1 }; + match self.screen_mut() + .on_event(event.relativized((0, offset))) + { + // If the event was ignored, + // it is our turn to play with it. + EventResult::Ignored => self.on_event(event), + EventResult::Consumed(None) => (), + EventResult::Consumed(Some(cb)) => cb(self), + } + } + }, + Interruption::Callback(cb) => { + cb.call_box(self); + }, + Interruption::Timeout => { } } } diff --git a/src/lib.rs b/src/lib.rs index 968b752..f3b4e90 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -67,6 +67,8 @@ extern crate enum_map; extern crate enumset; #[macro_use] extern crate log; +#[macro_use] +extern crate chan; #[cfg(any(feature = "ncurses", feature = "pancurses"))] #[macro_use] @@ -80,10 +82,6 @@ extern crate unicode_segmentation; extern crate unicode_width; extern crate xi_unicode; -#[cfg(feature = "termion")] -#[macro_use] -extern crate chan; - macro_rules! new_default( ($c:ty) => { impl Default for $c { From 0b6e5b6ad40853127a967b3ad857eff429f91be8 Mon Sep 17 00:00:00 2001 From: Alexandre Bury Date: Sun, 20 May 2018 10:42:52 -0700 Subject: [PATCH 2/5] Add event polling to Blt backend Basically simulates a constant set_fps(30) --- src/backend/blt.rs | 78 +++++++++++++++++++------------------------- src/backend/dummy.rs | 9 +++-- src/backend/mod.rs | 17 ++++++++-- src/cursive.rs | 7 +++- 4 files changed, 60 insertions(+), 51 deletions(-) diff --git a/src/backend/blt.rs b/src/backend/blt.rs index 91a7922..4969ca7 100644 --- a/src/backend/blt.rs +++ b/src/backend/blt.rs @@ -15,7 +15,7 @@ use std::collections::HashSet; use theme::{BaseColor, Color, ColorPair, Effect}; use vec::Vec2; use chan; -use std::thread; +use std::time::{Duration, Instant}; enum ColorRole { Foreground, @@ -23,28 +23,38 @@ enum ColorRole { } pub struct Backend { -} - -struct InputParser { - event_sink: chan::Sender, buttons_pressed: HashSet, mouse_position: Vec2, } -impl InputParser { - fn new(event_sink: chan::Sender) -> Self { - InputParser { - event_sink, +impl Backend { + pub fn init() -> Box { + terminal::open("Cursive", 80, 24); + terminal::set(terminal::config::Window::empty().resizeable(true)); + terminal::set(vec![ + terminal::config::InputFilter::Group { + group: terminal::config::InputFilterGroup::Keyboard, + both: false, + }, + terminal::config::InputFilter::Group { + group: terminal::config::InputFilterGroup::Mouse, + both: true, + }, + ]); + + let c = Backend { buttons_pressed: HashSet::new(), mouse_position: Vec2::zero(), - } + }; + + Box::new(c) } - fn parse_next(&mut self) { + fn parse_next(&mut self) -> Option { // TODO: we could add backend-specific controls here. // Ex: ctrl+mouse wheel cause window cellsize to change - let event = if let Some(ev) = terminal::wait_event() { + terminal::read_event().map(|ev| { match ev { BltEvent::Close => Event::Exit, BltEvent::Resize { .. } => Event::WindowResize, @@ -94,10 +104,7 @@ impl InputParser { Event::Refresh } } - } else { - Event::Refresh - }; - self.event_sink.send(event); + }) } fn blt_keycode_to_ev( @@ -223,28 +230,6 @@ impl InputParser { } } -impl Backend { - pub fn init() -> Box { - terminal::open("Cursive", 80, 24); - terminal::set(terminal::config::Window::empty().resizeable(true)); - terminal::set(vec![ - terminal::config::InputFilter::Group { - group: terminal::config::InputFilterGroup::Keyboard, - both: false, - }, - terminal::config::InputFilter::Group { - group: terminal::config::InputFilterGroup::Mouse, - both: true, - }, - ]); - - let c = Backend {}; - - Box::new(c) - } - -} - impl backend::Backend for Backend { fn finish(&mut self) { terminal::close(); @@ -320,14 +305,19 @@ 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: chan::Sender) { - let mut parser = InputParser::new(event_sink); + fn start_input_thread(&mut self, _event_sink: chan::Sender) { + } - thread::spawn(move || { - loop { - parser.parse_next(); + fn prepare_input(&mut self, event_sink: &chan::Sender, timeout: Duration) { + // Wait for up to `timeout_ms`. + let start = Instant::now(); + while start.elapsed() < timeout { + if let Some(event) = self.parse_next() { + event_sink.send(event); + return; } - }); + } + event_sink.send(Event::Refresh); } } diff --git a/src/backend/dummy.rs b/src/backend/dummy.rs index d104d1e..ff45055 100644 --- a/src/backend/dummy.rs +++ b/src/backend/dummy.rs @@ -4,7 +4,7 @@ use theme; use event; use vec::Vec2; -use std::thread; +use std::time::Duration; use chan; @@ -32,8 +32,11 @@ impl backend::Backend for Backend { (1, 1).into() } - fn start_input_thread(&mut self, event_sink: chan::Sender) { - thread::spawn(move || event_sink.send(event::Event::Exit)); + fn start_input_thread(&mut self, _event_sink: chan::Sender) { + } + + fn prepare_input(&mut self, event_sink: &chan::Sender, _timeout: Duration) { + event_sink.send(event::Event::Exit) } fn print_at(&self, _: Vec2, _: &str) {} diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 06b290d..818b885 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -14,6 +14,8 @@ use chan::Sender; use vec::Vec2; +use std::time::Duration; + pub mod dummy; pub mod termion; @@ -29,8 +31,20 @@ 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. fn start_input_thread(&mut self, event_sink: Sender); + /// 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, event_sink: &Sender, timeout: Duration) { + // Dummy implementation for most backends. + // Little trick to avoid unused variables. + let _ = event_sink; + let _ = timeout; + } + /// Refresh the screen. fn refresh(&mut self); @@ -40,9 +54,6 @@ pub trait Backend { /// Returns the screen size. fn screen_size(&self) -> Vec2; - // /// Gets a receiver for input events. - // fn event_receiver(&mut self) -> Receiver; - /// Main method used for printing fn print_at(&self, pos: Vec2, text: &str); diff --git a/src/cursive.rs b/src/cursive.rs index 335dcec..b1a9c5f 100644 --- a/src/cursive.rs +++ b/src/cursive.rs @@ -5,6 +5,7 @@ use event::{Callback, Event, EventResult}; use printer::Printer; use std::any::Any; use std::collections::HashMap; +use std::time::Duration; use std::path::Path; use theme; use vec::Vec2; @@ -40,6 +41,7 @@ pub struct Cursive { cb_sink: chan::Sender>, event_source: chan::Receiver, + event_sink: chan::Sender, } /// Describes one of the possible interruptions we should handle. @@ -122,7 +124,7 @@ impl Cursive { let (cb_sink, cb_source) = chan::async(); let (event_sink, event_source) = chan::async(); - backend.start_input_thread(event_sink); + backend.start_input_thread(event_sink.clone()); Cursive { fps: 0, @@ -136,6 +138,7 @@ impl Cursive { cb_source, cb_sink, event_source, + event_sink, backend: backend, } } @@ -573,6 +576,8 @@ impl Cursive { let input_channel = &self.event_source; let cb_channel = &self.cb_source; + self.backend.prepare_input(&self.event_sink, Duration::from_millis(30)); + if self.fps > 0 { let timeout = 1000 / self.fps; let timeout = chan::after_ms(timeout); From c8d39910ab24826a772ea5584db94f4f3e543ebf Mon Sep 17 00:00:00 2001 From: Alexandre Bury Date: Sun, 20 May 2018 10:55:13 -0700 Subject: [PATCH 3/5] Backend: add AtomicBool to stop input thread --- src/backend/blt.rs | 3 --- src/backend/curses/n.rs | 6 ++++-- src/backend/curses/pan.rs | 5 +++-- src/backend/dummy.rs | 3 --- src/backend/mod.rs | 8 +++++++- src/backend/termion.rs | 6 ++++-- src/cursive.rs | 10 +++++++++- 7 files changed, 27 insertions(+), 14 deletions(-) diff --git a/src/backend/blt.rs b/src/backend/blt.rs index 4969ca7..7c44037 100644 --- a/src/backend/blt.rs +++ b/src/backend/blt.rs @@ -305,9 +305,6 @@ 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: chan::Sender) { - } - fn prepare_input(&mut self, event_sink: &chan::Sender, timeout: Duration) { // Wait for up to `timeout_ms`. let start = Instant::now(); diff --git a/src/backend/curses/n.rs b/src/backend/curses/n.rs index da2908e..08effcf 100644 --- a/src/backend/curses/n.rs +++ b/src/backend/curses/n.rs @@ -15,6 +15,8 @@ use std::fs::File; use std::io; use std::io::{Write}; use std::thread; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; use libc; use chan; @@ -262,13 +264,13 @@ impl backend::Backend for Backend { ncurses::has_colors() } - fn start_input_thread(&mut self, event_sink: chan::Sender) { + fn start_input_thread(&mut self, event_sink: chan::Sender, running: Arc) { let mut parser = InputParser::new(event_sink); // Start an input thread thread::spawn(move || { // TODO: use an atomic boolean to stop the thread on finish - loop { + while running.load(Ordering::Relaxed) { parser.parse_next(); } }); diff --git a/src/backend/curses/pan.rs b/src/backend/curses/pan.rs index 98b2495..c4fea49 100644 --- a/src/backend/curses/pan.rs +++ b/src/backend/curses/pan.rs @@ -12,6 +12,7 @@ use vec::Vec2; use std::sync::Arc; use chan; use std::thread; +use std::sync::atomic::{AtomicBool, Ordering}; pub struct Backend { // Used @@ -419,11 +420,11 @@ 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: chan::Sender) { + fn start_input_thread(&mut self, event_sink: chan::Sender, running: Arc) { let mut input_parser = InputParser::new(event_sink, Arc::clone(&self.window)); thread::spawn(move || { - loop { + while running.load(Ordering::Relaxed) { input_parser.parse_next(); } }); diff --git a/src/backend/dummy.rs b/src/backend/dummy.rs index ff45055..e22bb7f 100644 --- a/src/backend/dummy.rs +++ b/src/backend/dummy.rs @@ -32,9 +32,6 @@ impl backend::Backend for Backend { (1, 1).into() } - fn start_input_thread(&mut self, _event_sink: chan::Sender) { - } - fn prepare_input(&mut self, event_sink: &chan::Sender, _timeout: Duration) { event_sink.send(event::Event::Exit) } diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 818b885..356a9dc 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -15,6 +15,8 @@ use chan::Sender; use vec::Vec2; use std::time::Duration; +use std::sync::Arc; +use std::sync::atomic::AtomicBool; pub mod dummy; @@ -32,7 +34,11 @@ pub trait Backend { fn finish(&mut self); /// Starts a thread to collect input and send it to the given channel. - fn start_input_thread(&mut self, event_sink: Sender); + fn start_input_thread(&mut self, event_sink: Sender, running: Arc) { + // Dummy implementation for some backends. + let _ = event_sink; + let _ = running; + } /// Prepares the backend to collect input. /// diff --git a/src/backend/termion.rs b/src/backend/termion.rs index 7a5aa7e..011c9e4 100644 --- a/src/backend/termion.rs +++ b/src/backend/termion.rs @@ -24,6 +24,8 @@ use std::io::{Stdout, Write}; use std::thread; use theme; use vec::Vec2; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; pub struct Backend { terminal: AlternateScreen>>, @@ -265,10 +267,10 @@ impl backend::Backend for Backend { ); } - fn start_input_thread(&mut self, event_sink: chan::Sender) { + fn start_input_thread(&mut self, event_sink: chan::Sender, running: Arc) { let mut parser = InputParser::new(event_sink); thread::spawn(move || { - loop { + while running.load(Ordering::Relaxed) { parser.parse_next(); } }); diff --git a/src/cursive.rs b/src/cursive.rs index b1a9c5f..4a3d30a 100644 --- a/src/cursive.rs +++ b/src/cursive.rs @@ -7,6 +7,8 @@ use std::any::Any; use std::collections::HashMap; use std::time::Duration; use std::path::Path; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; use theme; use vec::Vec2; use view::{self, Finder, IntoBoxedView, Position, View}; @@ -42,6 +44,8 @@ pub struct Cursive { event_source: chan::Receiver, event_sink: chan::Sender, + + input_running: Arc, } /// Describes one of the possible interruptions we should handle. @@ -124,7 +128,9 @@ impl Cursive { let (cb_sink, cb_source) = chan::async(); let (event_sink, event_source) = chan::async(); - backend.start_input_thread(event_sink.clone()); + let input_running = Arc::new(AtomicBool::new(true)); + + backend.start_input_thread(event_sink.clone(), input_running.clone()); Cursive { fps: 0, @@ -139,6 +145,7 @@ impl Cursive { cb_sink, event_source, event_sink, + input_running, backend: backend, } } @@ -768,6 +775,7 @@ impl Cursive { impl Drop for Cursive { fn drop(&mut self) { + self.input_running.store(false, Ordering::Relaxed); self.backend.finish(); } } From 70cddae4547424f16a375bbd0d2a66a39c3fc468 Mon Sep 17 00:00:00 2001 From: Alexandre Bury Date: Mon, 21 May 2018 22:51:10 -0700 Subject: [PATCH 4/5] Add comments --- src/backend/curses/n.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/backend/curses/n.rs b/src/backend/curses/n.rs index 08effcf..eba0e88 100644 --- a/src/backend/curses/n.rs +++ b/src/backend/curses/n.rs @@ -112,12 +112,15 @@ impl InputParser { let mut bare_event = mevent.bstate & ((1 << 25) - 1); let mut event = None; + // ncurses encodes multiple events in the same value. while bare_event != 0 { let single_event = 1 << bare_event.trailing_zeros(); bare_event ^= single_event; // Process single_event on_mouse_event(single_event as i32, |e| { + // Keep one event for later, + // send the rest through the channel. if event.is_none() { event = Some(e); } else { @@ -271,6 +274,7 @@ impl backend::Backend for Backend { thread::spawn(move || { // TODO: use an atomic boolean to stop the thread on finish while running.load(Ordering::Relaxed) { + // This sends events to the event sender. parser.parse_next(); } }); From 3f5b37951bb3328f06ec7503b8b08a95476e36a0 Mon Sep 17 00:00:00 2001 From: Alexandre Bury Date: Fri, 8 Jun 2018 19:48:01 -0700 Subject: [PATCH 5/5] Do not consume input before exit --- src/backend/curses/n.rs | 12 +++++++----- src/backend/curses/pan.rs | 10 +++++++--- src/backend/mod.rs | 6 ++---- src/backend/termion.rs | 12 ++++++++---- src/cursive.rs | 18 ++++++++++-------- 5 files changed, 34 insertions(+), 24 deletions(-) diff --git a/src/backend/curses/n.rs b/src/backend/curses/n.rs index eba0e88..c55f5a1 100644 --- a/src/backend/curses/n.rs +++ b/src/backend/curses/n.rs @@ -15,8 +15,6 @@ use std::fs::File; use std::io; use std::io::{Write}; use std::thread; -use std::sync::Arc; -use std::sync::atomic::{AtomicBool, Ordering}; use libc; use chan; @@ -267,15 +265,19 @@ impl backend::Backend for Backend { ncurses::has_colors() } - fn start_input_thread(&mut self, event_sink: chan::Sender, running: Arc) { + fn start_input_thread(&mut self, event_sink: chan::Sender, stops: chan::Receiver) { let mut parser = InputParser::new(event_sink); // Start an input thread thread::spawn(move || { - // TODO: use an atomic boolean to stop the thread on finish - while running.load(Ordering::Relaxed) { + loop { // This sends events to the event sender. parser.parse_next(); + + if stops.recv() != Some(false) { + // If the channel was closed or if `true` was sent, abort. + break; + } } }); } diff --git a/src/backend/curses/pan.rs b/src/backend/curses/pan.rs index c4fea49..2416406 100644 --- a/src/backend/curses/pan.rs +++ b/src/backend/curses/pan.rs @@ -12,7 +12,6 @@ use vec::Vec2; use std::sync::Arc; use chan; use std::thread; -use std::sync::atomic::{AtomicBool, Ordering}; pub struct Backend { // Used @@ -420,12 +419,17 @@ 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: chan::Sender, running: Arc) { + fn start_input_thread(&mut self, event_sink: chan::Sender, stops: chan::Receiver) { let mut input_parser = InputParser::new(event_sink, Arc::clone(&self.window)); thread::spawn(move || { - while running.load(Ordering::Relaxed) { + loop { input_parser.parse_next(); + + if stops.recv() != Some(false) { + // If the channel was closed or if `true` was sent, abort. + break; + } } }); diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 356a9dc..379abc5 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -10,13 +10,11 @@ use event; use theme; -use chan::Sender; +use chan::{Receiver, Sender}; use vec::Vec2; use std::time::Duration; -use std::sync::Arc; -use std::sync::atomic::AtomicBool; pub mod dummy; @@ -34,7 +32,7 @@ pub trait Backend { fn finish(&mut self); /// Starts a thread to collect input and send it to the given channel. - fn start_input_thread(&mut self, event_sink: Sender, running: Arc) { + fn start_input_thread(&mut self, event_sink: Sender, running: Receiver) { // Dummy implementation for some backends. let _ = event_sink; let _ = running; diff --git a/src/backend/termion.rs b/src/backend/termion.rs index 011c9e4..70f1db6 100644 --- a/src/backend/termion.rs +++ b/src/backend/termion.rs @@ -24,8 +24,6 @@ use std::io::{Stdout, Write}; use std::thread; use theme; use vec::Vec2; -use std::sync::Arc; -use std::sync::atomic::{AtomicBool, Ordering}; pub struct Backend { terminal: AlternateScreen>>, @@ -267,11 +265,17 @@ impl backend::Backend for Backend { ); } - fn start_input_thread(&mut self, event_sink: chan::Sender, running: Arc) { + fn start_input_thread(&mut self, event_sink: chan::Sender, stops: chan::Receiver) { let mut parser = InputParser::new(event_sink); thread::spawn(move || { - while running.load(Ordering::Relaxed) { + loop { + // This sends events to the event sender. parser.parse_next(); + + if stops.recv() != Some(false) { + // If the channel was closed or if `true` was sent, abort. + break; + } } }); } diff --git a/src/cursive.rs b/src/cursive.rs index 4a3d30a..b2a7b03 100644 --- a/src/cursive.rs +++ b/src/cursive.rs @@ -7,8 +7,6 @@ use std::any::Any; use std::collections::HashMap; use std::time::Duration; use std::path::Path; -use std::sync::Arc; -use std::sync::atomic::{AtomicBool, Ordering}; use theme; use vec::Vec2; use view::{self, Finder, IntoBoxedView, Position, View}; @@ -45,7 +43,8 @@ pub struct Cursive { event_source: chan::Receiver, event_sink: chan::Sender, - input_running: Arc, + // Sends true or false after each event. + stop_sink: chan::Sender, } /// Describes one of the possible interruptions we should handle. @@ -128,9 +127,9 @@ impl Cursive { let (cb_sink, cb_source) = chan::async(); let (event_sink, event_source) = chan::async(); - let input_running = Arc::new(AtomicBool::new(true)); + let (stop_sink, stop_source) = chan::async(); - backend.start_input_thread(event_sink.clone(), input_running.clone()); + backend.start_input_thread(event_sink.clone(), stop_source); Cursive { fps: 0, @@ -145,8 +144,8 @@ impl Cursive { cb_sink, event_source, event_sink, - input_running, - backend: backend, + backend, + stop_sink, } } @@ -758,6 +757,10 @@ impl Cursive { EventResult::Consumed(Some(cb)) => cb(self), } } + + // Ok, we processed the event. + // Now tell the backend whether he sould keep receiving. + self.stop_sink.send(!self.running); }, Interruption::Callback(cb) => { cb.call_box(self); @@ -775,7 +778,6 @@ impl Cursive { impl Drop for Cursive { fn drop(&mut self) { - self.input_running.store(false, Ordering::Relaxed); self.backend.finish(); } }