From 616fddad3e8557d3a4248fd25d00c256eab2c827 Mon Sep 17 00:00:00 2001 From: Jesse Luehrs Date: Sat, 13 Mar 2021 16:52:33 -0500 Subject: docs --- src/blocking/input.rs | 58 ++++++++++++++++++++++++++++---- src/blocking/output.rs | 24 ++++++++++++++ src/color.rs | 2 ++ src/error.rs | 6 ++++ src/input.rs | 60 ++++++++++++++++++++++++++++++---- src/key.rs | 6 ++++ src/lib.rs | 89 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/output.rs | 26 +++++++++++++++ 8 files changed, 259 insertions(+), 12 deletions(-) diff --git a/src/blocking/input.rs b/src/blocking/input.rs index 7a35f25..c668857 100644 --- a/src/blocking/input.rs +++ b/src/blocking/input.rs @@ -5,12 +5,16 @@ use std::os::unix::io::AsRawFd as _; use crate::private::Input as _; +/// Switches the terminal on `stdin` to raw mode, and restores it when this +/// object goes out of scope. pub struct RawGuard { termios: Option, } impl RawGuard { - #[allow(clippy::new_without_default)] + /// Switches the terminal on `stdin` to raw mode and returns a guard + /// object. This is typically called as part of + /// [`Input::new`](Input::new). pub fn new() -> Result { let stdin = std::io::stdin().as_raw_fd(); let termios = @@ -28,6 +32,7 @@ impl RawGuard { }) } + /// Switch back from raw mode early. pub fn cleanup(&mut self) -> Result<()> { if let Some(termios) = self.termios.take() { let stdin = std::io::stdin().as_raw_fd(); @@ -44,11 +49,18 @@ impl RawGuard { } impl Drop for RawGuard { + /// Calls `cleanup`. fn drop(&mut self) { let _ = self.cleanup(); } } +/// Manages handling terminal input from `stdin`. +/// +/// The primary interface provided is [`read_key`](Input::read_key). You can +/// additionally configure the types of keypresses you are interested in +/// through the `parse_*` methods. This configuration can be changed between +/// any two calls to [`read_key`](Input::read_key). pub struct Input { raw: Option, @@ -112,14 +124,17 @@ impl crate::private::Input for Input { } } -#[allow(clippy::new_without_default)] impl Input { + /// Creates a new `Input` instance containing a [`RawGuard`](RawGuard) + /// instance. pub fn new() -> Result { let mut self_ = Self::new_without_raw(); self_.raw = Some(RawGuard::new()?); Ok(self_) } + /// Creates a new `Input` instance without creating a + /// [`RawGuard`](RawGuard) instance. pub fn new_without_raw() -> Self { Self { raw: None, @@ -133,30 +148,61 @@ impl Input { } } + /// Removes the [`RawGuard`](RawGuard) instance stored in this `Input` + /// instance and returns it. This can be useful if you need to manage the + /// lifetime of the [`RawGuard`](RawGuard) instance separately. + pub fn take_raw_guard(&mut self) -> Option { + self.raw.take() + } + + /// Sets whether `read_key` should try to produce + /// [`String`](crate::Key::String) or [`Char`](crate::Key::Char) keys when + /// possible, rather than [`Bytes`](crate::Key::Bytes) or + /// [`Byte`](crate::Key::Byte) keys. Note that + /// [`Bytes`](crate::Key::Bytes) or [`Byte`](crate::Key::Byte) keys may + /// still be produced if the input fails to be parsed as UTF-8. Defaults + /// to true. pub fn parse_utf8(&mut self, parse: bool) { self.parse_utf8 = parse; } + /// Sets whether `read_key` should produce [`Ctrl`](crate::Key::Ctrl) keys + /// when possible, rather than [`Bytes`](crate::Key::Bytes) or + /// [`Byte`](crate::Key::Byte) keys. Defaults to true. pub fn parse_ctrl(&mut self, parse: bool) { self.parse_ctrl = parse; } + /// Sets whether `read_key` should produce [`Meta`](crate::Key::Meta) keys + /// when possible, rather than producing the + /// [`Escape`](crate::Key::Escape) key separately. Defaults to true. pub fn parse_meta(&mut self, parse: bool) { self.parse_meta = parse; } + /// Sets whether `read_key` should produce keys other than + /// [`String`](crate::Key::String), [`Char`](crate::Key::Char), + /// [`Bytes`](crate::Key::Bytes), [`Byte`](crate::Key::Byte), + /// [`Ctrl`](crate::Key::Ctrl), or [`Meta`](crate::Key::Meta). Defaults to + /// true. pub fn parse_special_keys(&mut self, parse: bool) { self.parse_special_keys = parse; } + /// Sets whether `read_key` should produce individual + /// [`Char`](crate::Key::Char) or [`Byte`](crate::Key::Byte) keys, rather + /// than combining them into [`String`](crate::Key::String) or + /// [`Bytes`](crate::Key::Bytes) keys when possible. When this is true, + /// [`String`](crate::Key::String) and [`Bytes`](crate::Key::Bytes) will + /// never be returned, and when this is false, [`Char`](crate::Key::Char) + /// and [`Byte`](crate::Key::Byte) will never be returned. Defaults to + /// true. pub fn parse_single(&mut self, parse: bool) { self.parse_single = parse; } - pub fn take_raw_guard(&mut self) -> Option { - self.raw.take() - } - + /// Reads a keypress from the terminal on `stdin`. Returns `Ok(None)` on + /// EOF. pub fn read_key(&mut self) -> Result> { self.fill_buf()?; diff --git a/src/blocking/output.rs b/src/blocking/output.rs index 83466f8..77e13d2 100644 --- a/src/blocking/output.rs +++ b/src/blocking/output.rs @@ -4,16 +4,22 @@ use std::io::Write as _; use crate::private::Output as _; +/// Switches the terminal on `stdout` to alternate screen mode, and restores +/// it when this object goes out of scope. pub struct ScreenGuard { cleaned_up: bool, } impl ScreenGuard { + /// Switches the terminal on `stdout` to alternate screen mode and returns + /// a guard object. This is typically called as part of + /// [`Output::new`](Output::new). pub fn new() -> Result { write_stdout(crate::INIT)?; Ok(Self { cleaned_up: false }) } + /// Switch back from alternate screen mode early. pub fn cleanup(&mut self) -> Result<()> { if self.cleaned_up { return Ok(()); @@ -24,11 +30,18 @@ impl ScreenGuard { } impl Drop for ScreenGuard { + /// Calls `cleanup`. fn drop(&mut self) { let _ = self.cleanup(); } } +/// Manages drawing to the terminal on `stdout`. +/// +/// Most functionality is provided by the [`Textmode`](crate::Textmode) trait. +/// You should call those trait methods to draw to the in-memory screen, and +/// then call [`refresh`](Output::refresh) when you want to update the +/// terminal on `stdout`. pub struct Output { screen: Option, @@ -57,12 +70,16 @@ impl crate::private::Output for Output { impl crate::Textmode for Output {} impl Output { + /// Creates a new `Output` instance containing a + /// [`ScreenGuard`](ScreenGuard) instance. pub fn new() -> Result { let mut self_ = Self::new_without_screen(); self_.screen = Some(ScreenGuard::new()?); Ok(self_) } + /// Creates a new `Output` instance without creating a + /// [`ScreenGuard`](ScreenGuard) instance. pub fn new_without_screen() -> Self { let (rows, cols) = match terminal_size::terminal_size() { Some((terminal_size::Width(w), terminal_size::Height(h))) => { @@ -80,10 +97,17 @@ impl Output { } } + /// Removes the [`ScreenGuard`](ScreenGuard) instance stored in this + /// `Output` instance and returns it. This can be useful if you need to + /// manage the lifetime of the [`ScreenGuard`](ScreenGuard) instance + /// separately. pub fn take_screen_guard(&mut self) -> Option { self.screen.take() } + /// Draws the in-memory screen to the terminal on `stdout`. This is done + /// using a diff mechanism to only update the parts of the terminal which + /// are different from the in-memory screen. pub fn refresh(&mut self) -> Result<()> { let diff = self.next().screen().state_diff(self.cur().screen()); write_stdout(&diff)?; diff --git a/src/color.rs b/src/color.rs index c2ba2b3..289bed2 100644 --- a/src/color.rs +++ b/src/color.rs @@ -1,3 +1,5 @@ +//! Constants for the basic 16 terminal colors. + pub const BLACK: vt100::Color = vt100::Color::Idx(0); pub const RED: vt100::Color = vt100::Color::Idx(1); pub const GREEN: vt100::Color = vt100::Color::Idx(2); diff --git a/src/error.rs b/src/error.rs index f2e774e..27f5bd5 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,16 +1,22 @@ +/// Type for errors returned by this crate. #[derive(thiserror::Error, Debug)] pub enum Error { + /// error reading from stdin #[error("error reading from stdin")] ReadStdin(#[source] std::io::Error), + /// error enabling terminal raw mode #[error("error enabling terminal raw mode")] SetRaw(#[source] nix::Error), + /// error restoring terminal from raw mode #[error("error restoring terminal from raw mode")] UnsetRaw(#[source] nix::Error), + /// error writing to stdout #[error("error writing to stdout")] WriteStdout(#[source] std::io::Error), } +/// Convenience wrapper for a `Result` using `textmode::Error`. pub type Result = std::result::Result; diff --git a/src/input.rs b/src/input.rs index abcdd7f..bd10b86 100644 --- a/src/input.rs +++ b/src/input.rs @@ -5,12 +5,16 @@ use std::os::unix::io::AsRawFd as _; use crate::private::Input as _; +/// Switches the terminal on `stdin` to raw mode, and restores it when this +/// object goes out of scope. pub struct RawGuard { termios: Option, } impl RawGuard { - #[allow(clippy::new_without_default)] + /// Switches the terminal on `stdin` to raw mode and returns a guard + /// object. This is typically called as part of + /// [`Input::new`](Input::new). pub async fn new() -> Result { let stdin = std::io::stdin().as_raw_fd(); let termios = blocking::unblock(move || { @@ -33,6 +37,7 @@ impl RawGuard { }) } + /// Switch back from raw mode early. pub async fn cleanup(&mut self) -> Result<()> { if let Some(termios) = self.termios.take() { let stdin = std::io::stdin().as_raw_fd(); @@ -52,6 +57,9 @@ impl RawGuard { } impl Drop for RawGuard { + /// Calls `cleanup`. Note that this may block, due to Rust's current lack + /// of an async drop mechanism. If this could be a problem, you should + /// call `cleanup` manually instead. fn drop(&mut self) { futures_lite::future::block_on(async { let _ = self.cleanup().await; @@ -59,6 +67,12 @@ impl Drop for RawGuard { } } +/// Manages handling terminal input from `stdin`. +/// +/// The primary interface provided is [`read_key`](Input::read_key). You can +/// additionally configure the types of keypresses you are interested in +/// through the `parse_*` methods. This configuration can be changed between +/// any two calls to [`read_key`](Input::read_key). pub struct Input { stdin: blocking::Unblock, raw: Option, @@ -123,14 +137,17 @@ impl crate::private::Input for Input { } } -#[allow(clippy::new_without_default)] impl Input { + /// Creates a new `Input` instance containing a [`RawGuard`](RawGuard) + /// instance. pub async fn new() -> Result { let mut self_ = Self::new_without_raw(); self_.raw = Some(RawGuard::new().await?); Ok(self_) } + /// Creates a new `Input` instance without creating a + /// [`RawGuard`](RawGuard) instance. pub fn new_without_raw() -> Self { Self { stdin: blocking::Unblock::new(std::io::stdin()), @@ -145,30 +162,61 @@ impl Input { } } + /// Removes the [`RawGuard`](RawGuard) instance stored in this `Input` + /// instance and returns it. This can be useful if you need to manage the + /// lifetime of the [`RawGuard`](RawGuard) instance separately. + pub fn take_raw_guard(&mut self) -> Option { + self.raw.take() + } + + /// Sets whether `read_key` should try to produce + /// [`String`](crate::Key::String) or [`Char`](crate::Key::Char) keys when + /// possible, rather than [`Bytes`](crate::Key::Bytes) or + /// [`Byte`](crate::Key::Byte) keys. Note that + /// [`Bytes`](crate::Key::Bytes) or [`Byte`](crate::Key::Byte) keys may + /// still be produced if the input fails to be parsed as UTF-8. Defaults + /// to true. pub fn parse_utf8(&mut self, parse: bool) { self.parse_utf8 = parse; } + /// Sets whether `read_key` should produce [`Ctrl`](crate::Key::Ctrl) keys + /// when possible, rather than [`Bytes`](crate::Key::Bytes) or + /// [`Byte`](crate::Key::Byte) keys. Defaults to true. pub fn parse_ctrl(&mut self, parse: bool) { self.parse_ctrl = parse; } + /// Sets whether `read_key` should produce [`Meta`](crate::Key::Meta) keys + /// when possible, rather than producing the + /// [`Escape`](crate::Key::Escape) key separately. Defaults to true. pub fn parse_meta(&mut self, parse: bool) { self.parse_meta = parse; } + /// Sets whether `read_key` should produce keys other than + /// [`String`](crate::Key::String), [`Char`](crate::Key::Char), + /// [`Bytes`](crate::Key::Bytes), [`Byte`](crate::Key::Byte), + /// [`Ctrl`](crate::Key::Ctrl), or [`Meta`](crate::Key::Meta). Defaults to + /// true. pub fn parse_special_keys(&mut self, parse: bool) { self.parse_special_keys = parse; } + /// Sets whether `read_key` should produce individual + /// [`Char`](crate::Key::Char) or [`Byte`](crate::Key::Byte) keys, rather + /// than combining them into [`String`](crate::Key::String) or + /// [`Bytes`](crate::Key::Bytes) keys when possible. When this is true, + /// [`String`](crate::Key::String) and [`Bytes`](crate::Key::Bytes) will + /// never be returned, and when this is false, [`Char`](crate::Key::Char) + /// and [`Byte`](crate::Key::Byte) will never be returned. Defaults to + /// true. pub fn parse_single(&mut self, parse: bool) { self.parse_single = parse; } - pub fn take_raw_guard(&mut self) -> Option { - self.raw.take() - } - + /// Reads a keypress from the terminal on `stdin`. Returns `Ok(None)` on + /// EOF. pub async fn read_key(&mut self) -> Result> { self.fill_buf().await?; diff --git a/src/key.rs b/src/key.rs index 8c97ad8..505817f 100644 --- a/src/key.rs +++ b/src/key.rs @@ -1,9 +1,12 @@ +/// Type representing a keypress. #[derive(Eq, PartialEq, Debug, Clone)] pub enum Key { String(String), Char(char), Bytes(Vec), Byte(u8), + /// The associated value will be a byte corresponding to the lowercase + /// letter for the control code. For instance, `^A` will be `Ctrl(b'a')`. Ctrl(u8), Meta(u8), Backspace, @@ -22,10 +25,13 @@ pub enum Key { Delete, PageUp, PageDown, + /// The associated value will be the number corresponding to the `F` key. + /// For instance, `F1` will be `F(1)`. F(u8), } impl Key { + /// Returns bytes generated by the given key press. pub fn into_bytes(self) -> Vec { use Key::*; match self { diff --git a/src/lib.rs b/src/lib.rs index ca58146..27723fb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,71 @@ #![allow(clippy::collapsible_if)] +//! `textmode` is a library for terminal interaction built on top of a real +//! terminal parsing library. It allows you to do arbitrary drawing operations +//! on an in-memory screen, and then update the visible terminal output to +//! reflect the in-memory screen via an optimized diff algorithm when you are +//! finished. Being built on a real terminal parsing library means that while +//! normal curses-like operations are available: +//! +//! ```no_run +//! use textmode::Textmode; +//! # #[cfg(feature = "async")] +//! # fn main() -> textmode::Result<()> { +//! # futures_lite::future::block_on(async { run().await }) +//! # } +//! # #[cfg(feature = "async")] +//! # async fn run() -> textmode::Result<()> { +//! let mut tm = textmode::Output::new().await?; +//! tm.clear(); +//! tm.move_to(5, 5); +//! tm.set_fgcolor(textmode::color::RED); +//! tm.write_str("foo"); +//! tm.refresh().await?; +//! # Ok(()) +//! # } +//! # #[cfg(not(feature = "async"))] +//! # fn main() -> textmode::Result<()> { +//! # let mut tm = textmode::blocking::Output::new()?; +//! # tm.clear(); +//! # tm.move_to(5, 5); +//! # tm.set_fgcolor(textmode::color::RED); +//! # tm.write_str("foo"); +//! # tm.refresh()?; +//! # Ok(()) +//! # } +//! ``` +//! +//! you can also write data containing arbitrary terminal escape codes to the +//! output and they will also do the right thing: +//! +//! ```no_run +//! # use textmode::Textmode; +//! # #[cfg(feature = "async")] +//! # fn main() -> textmode::Result<()> { +//! # futures_lite::future::block_on(async { run().await }) +//! # } +//! # #[cfg(feature = "async")] +//! # async fn run() -> textmode::Result<()> { +//! # let mut tm = textmode::Output::new().await?; +//! tm.write(b"\x1b[34m\x1b[3;9Hbar\x1b[m"); +//! tm.refresh().await?; +//! # Ok(()) +//! # } +//! # #[cfg(not(feature = "async"))] +//! # fn main() -> textmode::Result<()> { +//! # let mut tm = textmode::blocking::Output::new()?; +//! # tm.write(b"\x1b[34m\x1b[3;9Hbar\x1b[m"); +//! # tm.refresh()?; +//! # Ok(()) +//! # } +//! ``` +//! +//! This module is split into two main parts: [`Output`](Output) and +//! [`Input`](Input). See the documentation for those types for more details. +//! Additionally, the [`blocking`] module provides an equivalent interface +//! with blocking calls instead of async. + +/// Blocking interface. pub mod blocking; pub mod color; @@ -21,24 +87,32 @@ pub use input::{Input, RawGuard}; const INIT: &[u8] = b"\x1b7\x1b[?47h\x1b[2J\x1b[H\x1b[?25h"; const DEINIT: &[u8] = b"\x1b[?47l\x1b8\x1b[?25h"; +/// Provides the methods used to manipulate the in-memory screen. pub trait Textmode: private::Output { + /// Returns the in-memory screen itself. This is the screen that will be + /// drawn on the next call to `refresh`. fn screen(&self) -> &vt100::Screen { self.next().screen() } + /// Writes a sequence of bytes, potentially containing terminal escape + /// sequences, to the in-memory screen. fn write(&mut self, buf: &[u8]) { self.next_mut().process(buf); } + /// Sets the terminal size for the in-memory screen. fn set_size(&mut self, rows: u16, cols: u16) { self.cur_mut().set_size(rows, cols); self.next_mut().set_size(rows, cols); } + /// Writes a string of printable characters to the in-memory screen. fn write_str(&mut self, text: &str) { self.write(text.as_bytes()); } + /// Moves the in-memory screen's cursor. fn move_to(&mut self, row: u16, col: u16) { self.write(b"\x1b["); self.write_u16(row + 1); @@ -47,18 +121,23 @@ pub trait Textmode: private::Output { self.write(b"H"); } + /// Clears the in-memory screen. fn clear(&mut self) { self.write(b"\x1b[2J"); } + /// Clears the line containing the cursor on the in-memory screen. fn clear_line(&mut self) { self.write(b"\x1b[K"); } + /// Clears the in-memory screen's currently active drawing attributes. fn reset_attributes(&mut self) { self.write(b"\x1b[m"); } + /// Sets the foreground color for subsequent drawing operations to the + /// in-memory screen. fn set_fgcolor(&mut self, color: vt100::Color) { match color { vt100::Color::Default => { @@ -91,6 +170,8 @@ pub trait Textmode: private::Output { } } + /// Sets the background color for subsequent drawing operations to the + /// in-memory screen. fn set_bgcolor(&mut self, color: vt100::Color) { match color { vt100::Color::Default => { @@ -123,6 +204,8 @@ pub trait Textmode: private::Output { } } + /// Sets whether subsequent text drawn to the in-memory screen should be + /// bold. fn set_bold(&mut self, bold: bool) { if bold { self.write(b"\x1b[1m"); @@ -131,6 +214,8 @@ pub trait Textmode: private::Output { } } + /// Sets whether subsequent text drawn to the in-memory screen should be + /// italic. fn set_italic(&mut self, italic: bool) { if italic { self.write(b"\x1b[3m"); @@ -139,6 +224,8 @@ pub trait Textmode: private::Output { } } + /// Sets whether subsequent text drawn to the in-memory screen should be + /// underlined. fn set_underline(&mut self, underline: bool) { if underline { self.write(b"\x1b[4m"); @@ -147,6 +234,8 @@ pub trait Textmode: private::Output { } } + /// Sets whether subsequent text drawn to the in-memory screen should have + /// its colors inverted. fn set_inverse(&mut self, inverse: bool) { if inverse { self.write(b"\x1b[7m"); diff --git a/src/output.rs b/src/output.rs index 1e0ed43..4b2fb06 100644 --- a/src/output.rs +++ b/src/output.rs @@ -4,11 +4,16 @@ use futures_lite::io::AsyncWriteExt as _; use crate::private::Output as _; +/// Switches the terminal on `stdout` to alternate screen mode, and restores +/// it when this object goes out of scope. pub struct ScreenGuard { cleaned_up: bool, } impl ScreenGuard { + /// Switches the terminal on `stdout` to alternate screen mode and returns + /// a guard object. This is typically called as part of + /// [`Output::new`](Output::new). pub async fn new() -> Result { write_stdout( &mut blocking::Unblock::new(std::io::stdout()), @@ -18,6 +23,7 @@ impl ScreenGuard { Ok(Self { cleaned_up: false }) } + /// Switch back from alternate screen mode early. pub async fn cleanup(&mut self) -> Result<()> { if self.cleaned_up { return Ok(()); @@ -32,6 +38,9 @@ impl ScreenGuard { } impl Drop for ScreenGuard { + /// Calls `cleanup`. Note that this may block, due to Rust's current lack + /// of an async drop mechanism. If this could be a problem, you should + /// call `cleanup` manually instead. fn drop(&mut self) { futures_lite::future::block_on(async { let _ = self.cleanup().await; @@ -39,6 +48,12 @@ impl Drop for ScreenGuard { } } +/// Manages drawing to the terminal on `stdout`. +/// +/// Most functionality is provided by the [`Textmode`](crate::Textmode) trait. +/// You should call those trait methods to draw to the in-memory screen, and +/// then call [`refresh`](Output::refresh) when you want to update the +/// terminal on `stdout`. pub struct Output { stdout: blocking::Unblock, screen: Option, @@ -68,12 +83,16 @@ impl crate::private::Output for Output { impl crate::Textmode for Output {} impl Output { + /// Creates a new `Output` instance containing a + /// [`ScreenGuard`](ScreenGuard) instance. pub async fn new() -> Result { let mut self_ = Self::new_without_screen(); self_.screen = Some(ScreenGuard::new().await?); Ok(self_) } + /// Creates a new `Output` instance without creating a + /// [`ScreenGuard`](ScreenGuard) instance. pub fn new_without_screen() -> Self { let (rows, cols) = match terminal_size::terminal_size() { Some((terminal_size::Width(w), terminal_size::Height(h))) => { @@ -91,10 +110,17 @@ impl Output { } } + /// Removes the [`ScreenGuard`](ScreenGuard) instance stored in this + /// `Output` instance and returns it. This can be useful if you need to + /// manage the lifetime of the [`ScreenGuard`](ScreenGuard) instance + /// separately. pub fn take_screen_guard(&mut self) -> Option { self.screen.take() } + /// Draws the in-memory screen to the terminal on `stdout`. This is done + /// using a diff mechanism to only update the parts of the terminal which + /// are different from the in-memory screen. pub async fn refresh(&mut self) -> Result<()> { let diff = self.next().screen().state_diff(self.cur().screen()); write_stdout(&mut self.stdout, &diff).await?; -- cgit v1.2.3-54-g00ecf