From 3bc00f5f3f030acc8dbbf8018f6c2a99561ca709 Mon Sep 17 00:00:00 2001 From: tomsmeding Date: Sun, 10 Feb 2019 20:37:02 +0100 Subject: Update organisation and document --- build.rs | 1 + src/core.rs | 292 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 204 +++++------------------------------ src/widgets.rs | 5 + src/widgets/log.rs | 39 ++++--- src/widgets/menu.rs | 11 ++ src/widgets/prompt.rs | 35 ++++-- vendor/termio | 2 +- 8 files changed, 385 insertions(+), 204 deletions(-) create mode 100644 src/core.rs diff --git a/build.rs b/build.rs index 46c31e3..18c690a 100644 --- a/build.rs +++ b/build.rs @@ -41,6 +41,7 @@ fn main() { .blacklist_item("true_") .blacklist_item("false_") .blacklist_item("__bool_true_false_are_defined") + .blacklist_function("tprintf") .blacklist_function("lgw_addf") .generate() .expect("Error generating bindings using bindgen"); diff --git a/src/core.rs b/src/core.rs new file mode 100644 index 0000000..9e67722 --- /dev/null +++ b/src/core.rs @@ -0,0 +1,292 @@ +use std::char; +use std::io; +use std::marker::PhantomData; +use std::sync::atomic::{AtomicBool, Ordering}; + +use crate::bindings; +use crate::util; + +pub const KEY_TAB: i32 = bindings::KEY_TAB as i32; +pub const KEY_LF: i32 = bindings::KEY_LF as i32; +pub const KEY_CR: i32 = bindings::KEY_CR as i32; +pub const KEY_ESC: i32 = bindings::KEY_ESC as i32; +pub const KEY_BACKSPACE: i32 = bindings::KEY_BACKSPACE as i32; + +pub const KEY_RIGHT: i32 = bindings::KEY_RIGHT as i32; +pub const KEY_UP: i32 = bindings::KEY_UP as i32; +pub const KEY_LEFT: i32 = bindings::KEY_LEFT as i32; +pub const KEY_DOWN: i32 = bindings::KEY_DOWN as i32; +pub const KEY_PAGEUP: i32 = bindings::KEY_PAGEUP as i32; +pub const KEY_PAGEDOWN: i32 = bindings::KEY_PAGEDOWN as i32; +pub const KEY_DELETE: i32 = bindings::KEY_DELETE as i32; +pub const KEY_SHIFTTAB: i32 = bindings::KEY_SHIFTTAB as i32; + +/// Modifier combinator. +pub const KEY_CTRL: i32 = bindings::KEY_CTRL as i32; +/// Modifier combinator. +pub const KEY_SHIFT: i32 = bindings::KEY_SHIFT as i32; +/// Modifier combinator. +pub const KEY_ALT: i32 = bindings::KEY_ALT as i32; +/// Modifier combinator. +pub const KEY_CTRLALT: i32 = bindings::KEY_CTRLALT as i32; +/// Modifier combinator. +pub const KEY_CTRLSHIFT: i32 = bindings::KEY_CTRLSHIFT as i32; + +/// A style descriptor for a cell on the screen. +#[derive(Debug, Copy, Clone)] +pub struct Style { + /// The foreground color; valid values are 0, 1, ..., 7 (the eight ANSI + /// colors) and 9 (the default color). + pub fg: u8, + + /// The background color; valid values are 0, 1, ..., 7 (the eight ANSI + /// colors) and 9 (the default color). + pub bg: u8, + + /// Whether the character in the cell should be printed bold. If this is + /// enabled, the terminal generally uses the bright version of the + /// foreground color instead of the normal version. Therefore, there are + /// effectively 16, not 8, foreground colors (in addition to the default + /// color, 9). + pub bold: bool, + + /// Whether to display an underline. + pub ul: bool +} + +/// RAII struct for keyboard-input initialization +/// +/// Only one instance of this struct can be active at a time. Attempting to +/// create a second instance will panic. +pub struct Keyboard { + _phantom: PhantomData<*mut u8>, +} + +static KEYBOARD_REFCOUNT: AtomicBool = AtomicBool::new(false); + +impl Keyboard { + /// Grab the keyboard, i.e. enable raw mode. + /// + /// This disables automatic printing of typed characters (echo), and allows + /// reading a single character even when a newline has not yet been + /// entered. + /// + /// If `nosigkeys` is true, interrupt keycodes like ctrl-C, ctrl-\ and + /// ctrl-Z will no longer be handled by the shell but will be available to + /// be read using `get_key()` just like the other keys. + pub fn grab(nosigkeys: bool) -> Self { + let prev = KEYBOARD_REFCOUNT.compare_and_swap( + false, true, Ordering::SeqCst); + if prev { + panic!("Can only hold one Keyboard struct at a time!"); + } + + unsafe { bindings::initkeyboard(nosigkeys); } + Self { _phantom: PhantomData } + } + + /// Explicitly release the keyboard. + /// + /// This prevents further use of the Keyboard functionality and + /// simultaneously forces its lifetime to extend until this call, delaying + /// deinitialization of the keyboard until this call. + pub fn end(self) {} + + // None on EOF, otherwise a combination of the KEY_* constants + pub fn get_key(&self) -> io::Result> { + match unsafe { bindings::tgetkey() } { + -2 => Err(io::Error::last_os_error()), + -1 => Ok(None), + value => Ok(Some(value)), + } + } + + pub fn get_line(&self) -> Option { + let ptr = unsafe { bindings::tgetline() }; + if ptr.is_null() { + None + } else { + unsafe { Some(util::string_from_utf8_charp(ptr)) } + } + } +} + +impl Drop for Keyboard { + fn drop(&mut self) { + let prev = KEYBOARD_REFCOUNT.compare_and_swap( + true, false, Ordering::SeqCst); + if !prev { + panic!("Internal: No Keyboard was held on Drop of a Keyboard?"); + } + + unsafe { bindings::endkeyboard(); } + } +} + +/// RAII struct for screen-drawing initialization +/// +/// Only one instance of this struct can be active at a time. Attempting to +/// create a second instance will panic. +pub struct Screen<'a> { + _keyboard: &'a Keyboard, +} + +static SCREEN_REFCOUNT: AtomicBool = AtomicBool::new(false); + +impl<'a> Screen<'a> { + /// Grab the screen, i.e. switch to the alternative screen and allocate draw + /// buffers. + /// + /// This function takes and stores a Keyboard reference to ensure that the + /// keyboard is initialized while the screen is grabbed. + pub fn grab<'b: 'a>(keyboard: &'b Keyboard) -> Self { + let prev = SCREEN_REFCOUNT.compare_and_swap( + false, true, Ordering::SeqCst); + if prev { + panic!("Can only hold one Screen struct at a time!"); + } + + unsafe { bindings::initscreen(); } + Self { _keyboard: keyboard } + } + + pub fn end(self) {} + + /// Install ctrl-L redraw handler. + /// + /// If the argument is true, then upon reading ctrl-L in + /// `Keyboard::get_key()`, a full redraw is issued, and the the call blocks + /// for the next key. Note that if the user only types ctrl-L, this means + /// that `get_key` may block while you don't expect it. + pub fn install_cl_handler(&self, install: bool) { + unsafe { bindings::installCLhandler(install); } + } + + /// Install redraw handler. + /// + /// The argument indicates whether the redraw is a full redraw. The handler + /// is called before the redraw is performed. Please do not trigger redraws + /// from within the handler. + pub fn on_redraw(&self, handler: extern "C" fn(full_redraw: bool)) { + unsafe { bindings::installredrawhandler(Some(handler)); } + } + + pub fn clear(&self) { + unsafe { bindings::clearscreen(); } + } + + /// Update style state. + /// + /// This modifies the style for the characters printed from now on. Note + /// that this does not write anything to the terminal. + pub fn set_style(&self, style: Style) { + let sty = bindings::Style { + fg: style.fg as i32, + bg: style.bg as i32, + bold: style.bold, + ul: style.ul, + }; + unsafe { bindings::setstyle(&sty as *const bindings::Style); } + } + + /// Update partial style state. see `set_style()`. + pub fn set_fg(&self, fg: u8) { + unsafe { bindings::setfg(fg as i32); } + } + + /// Update partial style state. see `set_style()`. + pub fn set_bg(&self, bg: u8) { + unsafe { bindings::setbg(bg as i32); } + } + + /// Update partial style state. see `set_style()`. + pub fn set_bold(&self, bold: bool) { + unsafe { bindings::setbold(bold); } + } + + /// Update partial style state. see `set_style()`. + pub fn set_ul(&self, ul: bool) { + unsafe { bindings::setul(ul); } + } + + pub fn putc(&self, c: char) { + unsafe { bindings::tputc(c as i32); } + } + + /// Newlines ('\n') return to the column on which printing started. + pub fn print(&self, s: &str) { + unsafe { bindings::tprint(s.as_bytes().as_ptr() as *const i8); } + } + + pub fn fill_rect(&self, lefttop: (u32, u32), size: (u32, u32), c: char) { + unsafe { bindings::fillrect( + lefttop.0 as i32, lefttop.1 as i32, + size.0 as i32, size.1 as i32, + c as i32); } + } + + pub fn redraw(&self) { + unsafe { bindings::redraw(); } + } + + pub fn redraw_full(&self) { + unsafe { bindings::redrawfull(); } + } + + /// Scrolls a rectangle on the screen by a signed number of lines. + pub fn scroll_term(&self, lefttop: (u32, u32), size: (u32, u32), amount: i32) { + unsafe { bindings::scrollterm( + lefttop.0 as i32, lefttop.1 as i32, + size.0 as i32, size.1 as i32, + amount); } + } + + pub fn get_buffer_char(&self, pos: (u32, u32)) -> char { + unsafe { + char::from_u32( + bindings::getbufferchar(pos.0 as i32, pos.1 as i32) as u32 + ).unwrap() + } + } + + /// Updates state; does not actually move the cursor yet (use `redraw()`). + pub fn move_to(&self, pos: (u32, u32)) { + unsafe { bindings::moveto(pos.0 as i32, pos.1 as i32); } + } + + /// Save the (internal) cursor position on a stack. + pub fn push_cursor(&self) { + unsafe { bindings::pushcursor(); } + } + + /// Pop the (internal) cursor position from the stack. + pub fn pop_cursor(&self) { + unsafe { bindings::popcursor(); } + } + + /// Takes effect immediately! + pub fn cursor_visible(&self, visible: bool) { + unsafe { bindings::cursorvisible(visible); } + } +} + +impl<'a> Drop for Screen<'a> { + fn drop(&mut self) { + let prev = SCREEN_REFCOUNT.compare_and_swap( + true, false, Ordering::SeqCst); + if !prev { + panic!("Internal: No Screen was held on Drop of a Screen?"); + } + + unsafe { bindings::endscreen(); } + } +} + +pub fn get_term_size() -> (u32, u32) { + let sz = unsafe { bindings::gettermsize() }; + (sz.w as u32, sz.h as u32) +} + +pub fn bel() { + unsafe { bindings::bel(); } +} diff --git a/src/lib.rs b/src/lib.rs index 7a17a0f..b092b85 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,183 +1,31 @@ -use std::char; -use std::io; +//! This crate contains bindings to the C library [termio][1]. Most functions +//! wrapped are methods on the `Screen` or `Keyboard` structs, while others take +//! one of these structs as an argument. This organisation ensures that e.g. the +//! screen can only be redrawn while the double-buffered screen is actually +//! active, and that keys can only be read from the input while the input stream +//! is actually in raw mode. +//! +//! The `widgets` submodule contains the built-in widget modules in termio. +//! +//! All callbacks are `extern "C"` functions, and thus do not accept generic +//! `Fn` instances. This is due to a limitation of the original termio API. +//! +//! In this crate, terminal screen positions and sizes are represented using a +//! tuple `(x, y): (u32, u32)`. Positions are zero-based. +//! +//! The `KEY_` constants describe the return values from `Keyboard::get_key()`. +//! To create key combinations, e.g. ctrl-A, use addition, e.g. `KEY_CTRL + +//! 'A'`. +//! +//! **Please be aware that to get anything on the screen, you need to call +//! `redraw()`.** +//! +//! [1]: https://github.com/tomsmeding/termio mod bindings; mod util; -pub mod widgets; - -// #[cfg(test)] -// mod tests { -// #[test] -// fn it_works() { -// assert_eq!(2 + 2, 4); -// } -// } - -// All positions are represented using a tuple (x, y): (u32, u32). -// Positions are zero-based. - -pub const KEY_TAB: i32 = bindings::KEY_TAB as i32; -pub const KEY_LF: i32 = bindings::KEY_LF as i32; -pub const KEY_CR: i32 = bindings::KEY_CR as i32; -pub const KEY_ESC: i32 = bindings::KEY_ESC as i32; -pub const KEY_BACKSPACE: i32 = bindings::KEY_BACKSPACE as i32; - -pub const KEY_RIGHT: i32 = bindings::KEY_RIGHT as i32; -pub const KEY_UP: i32 = bindings::KEY_UP as i32; -pub const KEY_LEFT: i32 = bindings::KEY_LEFT as i32; -pub const KEY_DOWN: i32 = bindings::KEY_DOWN as i32; -pub const KEY_PAGEUP: i32 = bindings::KEY_PAGEUP as i32; -pub const KEY_PAGEDOWN: i32 = bindings::KEY_PAGEDOWN as i32; -pub const KEY_DELETE: i32 = bindings::KEY_DELETE as i32; -pub const KEY_SHIFTTAB: i32 = bindings::KEY_SHIFTTAB as i32; - -pub const KEY_CTRL: i32 = bindings::KEY_CTRL as i32; -pub const KEY_SHIFT: i32 = bindings::KEY_SHIFT as i32; -pub const KEY_ALT: i32 = bindings::KEY_ALT as i32; -pub const KEY_CTRLALT: i32 = bindings::KEY_CTRLALT as i32; -pub const KEY_CTRLSHIFT: i32 = bindings::KEY_CTRLSHIFT as i32; - -#[derive(Debug, Copy, Clone)] -pub struct Style { - pub fg: u8, // 0-7, 9 - pub bg: u8, // 0-7, 9 - pub bold: bool, - pub ul: bool -} - -pub fn init_keyboard(nosigkeys: bool) { - unsafe { bindings::initkeyboard(nosigkeys); } -} - -pub fn end_keyboard() { - unsafe { bindings::endkeyboard(); } -} - -pub fn init_screen() { - unsafe { bindings::initscreen(); } -} - -pub fn end_screen() { - unsafe { bindings::endscreen(); } -} - -pub fn install_cl_handler(install: bool) { - unsafe { bindings::installCLhandler(install); } -} - -pub fn install_redraw_handler(handler: extern "C" fn(full_redraw: bool)) { - unsafe { bindings::installredrawhandler(Some(handler)); } -} - -pub fn clear_screen() { - unsafe { bindings::clearscreen(); } -} - -pub fn get_term_size() -> (u32, u32) { - let sz = unsafe { bindings::gettermsize() }; - (sz.w as u32, sz.h as u32) -} - -pub fn set_style(style: Style) { - let sty = bindings::Style { - fg: style.fg as i32, - bg: style.bg as i32, - bold: style.bold, - ul: style.ul, - }; - unsafe { bindings::setstyle(&sty as *const bindings::Style); } -} - -pub fn set_fg(fg: u8) { - unsafe { bindings::setfg(fg as i32); } -} -pub fn set_bg(bg: u8) { - unsafe { bindings::setbg(bg as i32); } -} - -pub fn set_bold(bold: bool) { - unsafe { bindings::setbold(bold); } -} - -pub fn set_ul(ul: bool) { - unsafe { bindings::setul(ul); } -} - -pub fn putc(c: char) { - unsafe { bindings::tputc(c as i32); } -} - -pub fn print(s: &str) { - unsafe { bindings::tprintf( - "%s".as_bytes().as_ptr() as *const i8, - s.as_bytes().as_ptr() as *const i8); } -} - -pub fn fill_rect(lefttop: (u32, u32), size: (u32, u32), c: char) { - unsafe { bindings::fillrect( - lefttop.0 as i32, lefttop.1 as i32, - size.0 as i32, size.1 as i32, - c as i32); } -} - -pub fn redraw() { - unsafe { bindings::redraw(); } -} - -pub fn redraw_full() { - unsafe { bindings::redrawfull(); } -} - -pub fn scroll_term(lefttop: (u32, u32), size: (u32, u32), c: char) { - unsafe { bindings::scrollterm( - lefttop.0 as i32, lefttop.1 as i32, - size.0 as i32, size.1 as i32, - c as i32); } -} - -pub fn get_buffer_char(pos: (u32, u32)) -> char { - unsafe { - char::from_u32( - bindings::getbufferchar(pos.0 as i32, pos.1 as i32) as u32 - ).unwrap() - } -} - -pub fn move_to(pos: (u32, u32)) { - unsafe { bindings::moveto(pos.0 as i32, pos.1 as i32); } -} - -pub fn push_cursor() { - unsafe { bindings::pushcursor(); } -} - -pub fn pop_cursor() { - unsafe { bindings::popcursor(); } -} - -pub fn bel() { - unsafe { bindings::bel(); } -} - -pub fn cursor_visible(visible: bool) { - unsafe { bindings::cursorvisible(visible); } -} - -// None on EOF, otherwise a combination of the KEY_* constants -pub fn get_key() -> io::Result> { - match unsafe { bindings::tgetkey() } { - -2 => Err(io::Error::last_os_error()), - -1 => Ok(None), - value => Ok(Some(value)), - } -} +mod core; +pub mod widgets; -pub fn get_line() -> Option { - let ptr = unsafe { bindings::tgetline() }; - if ptr.is_null() { - None - } else { - unsafe { Some(util::string_from_utf8_charp(ptr)) } - } -} +pub use crate::core::*; diff --git a/src/widgets.rs b/src/widgets.rs index d8c8e2e..fa79aab 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -1,3 +1,8 @@ +//! The built-in widgets in termio. +//! +//! The structures and functions for each of the widgets live in their +//! respective submodules below. + pub mod log; pub mod prompt; pub mod menu; diff --git a/src/widgets/log.rs b/src/widgets/log.rs index 8bd240a..4966cc0 100644 --- a/src/widgets/log.rs +++ b/src/widgets/log.rs @@ -1,27 +1,44 @@ +//! Logging events in a scrolling list. +//! +//! Events are logged in an upwards scrolled box that forgets about the events +//! that move off-screen. The logger may optionally add timestamps to events. +//! +//! To create a log widget, make a `LogBuilder`, optionally modify properties, +//! and then use its `create` method to create a `Log` object. + use std::ptr; use crate::bindings; +use crate::core::Screen; pub struct Log { ptr: *mut bindings::Logwidget, } -pub struct LogBuilder<'a> { +pub struct LogBuilder<'a, 'b> { + _screen: &'a Screen<'b>, lefttop: (u32, u32), size: (u32, u32), title: Option<&'a str>, timestamps: bool, } -impl<'a> LogBuilder<'a> { - pub fn add_title<'s: 'a>(&'a mut self, title: &'s str) - -> &'a mut LogBuilder<'a> { +impl<'a, 'b> LogBuilder<'a, 'b> { + pub fn new(screen: &'a Screen<'b>, lefttop: (u32, u32), size: (u32, u32)) -> Self { + LogBuilder { + _screen: screen, + lefttop, size, + title: None, + timestamps: false, + } + } + + pub fn add_title<'s: 'a>(&mut self, title: &'s str) -> &mut Self { self.title = Some(title); self } - pub fn set_timestamps(&'a mut self, timestamps: bool) - -> &'a mut LogBuilder<'a> { + pub fn set_timestamps(&mut self, timestamps: bool) -> &mut Self { self.timestamps = timestamps; self } @@ -43,14 +60,8 @@ impl<'a> LogBuilder<'a> { } impl Log { - pub fn new<'a>(lefttop: (u32, u32), size: (u32, u32)) -> LogBuilder<'a> { - LogBuilder { - lefttop, size, - title: None, - timestamps: false, - } - } - + /// Called automatically; should only be needed if something else overwrote + /// the widget. pub fn redraw(&self) { unsafe { bindings::lgw_redraw(self.ptr); } } diff --git a/src/widgets/menu.rs b/src/widgets/menu.rs index fe899e3..f90e62b 100644 --- a/src/widgets/menu.rs +++ b/src/widgets/menu.rs @@ -1,3 +1,9 @@ +//! A selection menu. +//! +//! The configuration for a menu is encapsulated in a `Data` struct, which +//! contains some number of `Item` structs which describe the individual items +//! to be chosen between. + use std::ffi::c_void; use crate::bindings; @@ -46,10 +52,15 @@ impl Menu { } } + /// Called automatically; should only be needed if something else overwrote + /// the widget. pub fn redraw(&self) { unsafe { bindings::menu_redraw(self.ptr); } } + /// Process the given key in the context of the menu. + /// + /// The key should be a return value from `Keyboard::get_key()`. pub fn handle_key(&mut self, key: i32) -> KeyResult { match unsafe { bindings::menu_handlekey(self.ptr, key) } { bindings::Menukey_MENUKEY_HANDLED => KeyResult::Handled, diff --git a/src/widgets/prompt.rs b/src/widgets/prompt.rs index 0690840..31c8630 100644 --- a/src/widgets/prompt.rs +++ b/src/widgets/prompt.rs @@ -1,21 +1,36 @@ +//! A box for getting a line of text input. +//! +//! To create a prompt widget, make a `PromptBuilder`, optionally modify +//! properties, and then use its `create` method to create a `Prompt` object. + use std::ptr; use crate::bindings; +use crate::core::Screen; use crate::util; pub struct Prompt { ptr: *mut bindings::Promptwidget, } -pub struct PromptBuilder<'a> { +pub struct PromptBuilder<'a, 'b> { + _screen: &'a Screen<'b>, lefttop: (u32, u32), width: u32, title: Option<&'a str>, } -impl<'a> PromptBuilder<'a> { - pub fn add_title<'s: 'a>(&'a mut self, title: &'s str) - -> &'a mut PromptBuilder<'a> { +impl<'a, 'b> PromptBuilder<'a, 'b> { + pub fn new(screen: &'a Screen<'b>, lefttop: (u32, u32), width: u32) -> Self { + PromptBuilder { + _screen: screen, + lefttop, width, + title: None, + } + } + + pub fn add_title<'s: 'a>(&mut self, title: &'s str) + -> &mut Self { self.title = Some(title); self } @@ -36,17 +51,15 @@ impl<'a> PromptBuilder<'a> { } impl Prompt { - pub fn new<'a>(lefttop: (u32, u32), width: u32) -> PromptBuilder<'a> { - PromptBuilder { - lefttop, width, - title: None, - } - } - + /// Called automatically; should only be needed if something else overwrote + /// the widget. pub fn redraw(&self) { unsafe { bindings::prw_redraw(self.ptr); } } + /// Input string if , None otherwise. + /// + /// The key should be a return value from `Keyboard::get_key()`. pub fn handle_key(&mut self, key: i32) -> Option { let ptr = unsafe { bindings::prw_handlekey(self.ptr, key) }; if ptr.is_null() { diff --git a/vendor/termio b/vendor/termio index e2c2f93..0f498cf 160000 --- a/vendor/termio +++ b/vendor/termio @@ -1 +1 @@ -Subproject commit e2c2f93cd657ce9a40bcfd6c085328ac54e7a697 +Subproject commit 0f498cf54d8ecbfc81cf8b03ed2fcf3995b513f3 -- cgit v1.2.3