-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- add support for lighweight terminal I/O on Unix only
- add small command line utility for experimenting with terminal I/O - add support for nicely rendering terminal input
- Loading branch information
Showing
7 changed files
with
584 additions
and
75 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
#![cfg(target_family = "unix")] | ||
|
||
use prettypretty::termio::{render, Terminal, TERMINAL_TIMEOUT}; | ||
use prettypretty::trans::ThemeEntry; | ||
use std::io::{Read, Result, Write}; | ||
|
||
pub fn main() -> Result<()> { | ||
let terminal = Terminal::open()?.cbreak_mode(TERMINAL_TIMEOUT)?; | ||
let mut reader = terminal.reader(); | ||
let mut writer = terminal.writer(); | ||
let mut entries = ThemeEntry::all(); | ||
|
||
write!( | ||
writer, | ||
"press ‹t› to query rotating theme color, ‹q› to quit\r\n\r\n" | ||
)?; | ||
|
||
let mut iterations = 0; | ||
loop { | ||
iterations += 1; | ||
if 1000 <= iterations { | ||
write!(writer, "✋")?; | ||
break; | ||
} | ||
|
||
let mut buffer = [0; 32]; | ||
let count = reader.read(&mut buffer)?; | ||
if count == 0 { | ||
write!(writer, "◦")?; | ||
continue; | ||
} | ||
|
||
write!(writer, "〈")?; | ||
let mut terminate = false; | ||
let mut query = None; | ||
|
||
for b in buffer.iter().take(count) { | ||
render(*b, &mut writer)?; | ||
|
||
if *b == b'q' { | ||
terminate = true; | ||
} else if *b == b't' { | ||
let mut entry = entries.next(); | ||
if entry.is_none() { | ||
entries = ThemeEntry::all(); | ||
entry = entries.next(); | ||
} | ||
|
||
query = Some(entry.unwrap()); | ||
} | ||
} | ||
|
||
write!(writer, "〉")?; | ||
|
||
if terminate { | ||
break; | ||
} else if let Some(entry) = query { | ||
write!(writer, "{}", entry)?; | ||
} | ||
} | ||
|
||
terminal.restore()?; | ||
println!("\n\nbye bye!"); | ||
|
||
Ok(()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,45 +1,93 @@ | ||
//! Utilities for bringing your own terminal I/O. | ||
//! Terminal integration. | ||
//! | ||
//! Bringing your own terminal I/O sounds great on paper. It doesn't force sync | ||
//! or async I/O on your application. If async, it doesn't lock down the | ||
//! (possibly wrong) runtime. In short, it promises flexibility straight out of | ||
//! the crate. But the reality of bringing your own terminal I/O gets gnarly | ||
//! real fast because the on-the-wire protocol has none of the niceties of | ||
//! modern network protocols. It's just a stream of bytes with embedded ANSI | ||
//! escape sequences. | ||
//! Bringing your own terminal I/O sounds great at first. It doesn't force sync | ||
//! or async I/O on your application. If your application is async, it doesn't | ||
//! pick the runtime for you. In short, it promises flexibility straight out of | ||
//! the crate. But without additional library support, bringing your own | ||
//! terminal I/O also gets gnarly real fast. That's because command line | ||
//! applications communicate with the terminal (really the terminal emulator) | ||
//! through a protocol (bytes with embedded ANSI escape sequences) that has none | ||
//! of the niceties of actual network protocols. Notably, it doesn't just lack | ||
//! framing, but whether, say, 0x1b represents a press of the escape key or the | ||
//! first byte of an ANSI escape sequence is entirely dependent on context. | ||
//! | ||
//! This module provides the low-level building blocks for processing this | ||
//! protocol, i.e., writing and reading escape sequences: | ||
//! | ||
//! * [`VtScanner`] implements the state machines for recognizing ANSI escape | ||
//! sequences. | ||
//! * [`Control`] enumerates the different kinds of ANSI escape sequences and | ||
//! their initial bytes. | ||
//! * [`Action`] enumerates the different ways applications react to state | ||
//! machine transitions. | ||
//! # Integration of Terminal I/O | ||
//! | ||
//! The documentation for [`VtScanner`] includes example code for querying a | ||
//! terminal for its theme colors and integrating with the | ||
//! [`trans`](crate::trans) module's [`ThemeEntry`](crate::trans::ThemeEntry) | ||
//! abstraction. | ||
//! Producing a protocol stream is as simple as writing to standard output; | ||
//! that's why the displays for [`Style`](crate::style::Style) and | ||
//! [`ThemeEntry`](crate::trans::ThemeEntry) produce ANSI escape sequences. | ||
//! However, consuming such a stream requires three features: | ||
//! | ||
//! 1. [`Terminal`] lets the application disable a terminal's line discipline | ||
//! and restore it on exit again. | ||
//! 2. [`TerminalReader`] provides the ability to read a terminal's input | ||
//! stream without blocking indefinitely (with [`Terminal`] doing most of | ||
//! the work). | ||
//! 3. [`VtScanner`] parses a terminal's byte stream into characters and ANSI | ||
//! escape sequences, while ignoring malformed byte sequences. | ||
//! | ||
//! # More Generally: Sans I/O | ||
//! [`TerminalReader`] and the corresponding [`TerminalWriter`] are accessible | ||
//! through [`Terminal::reader`] and [`Terminal::writer`]. Alas, like | ||
//! [`Terminal`], they are only available on Unix-like operating systems | ||
//! supported by the [libc](https://github.com/rust-lang/libc) crate. If your | ||
//! application requires async I/O or Windows support, please consider using a | ||
//! more fully-featured terminal crate such as | ||
//! [Crossterm](https://github.com/crossterm-rs/crossterm). | ||
//! | ||
//! | ||
//! # Timing Out Reads | ||
//! | ||
//! There are at least three different approaches to supporting timeouts when | ||
//! reading from the terminal: | ||
//! | ||
//! 1. The first approach relies on the operating system's polling mechanism, | ||
//! such as `epoll` or `kqueue`. However, polling for a single resource from | ||
//! within a library seems like an antipattern. Also, macOS supports | ||
//! `select` only when polling devices including terminals. | ||
//! 2. The second approach uses a helper thread that uses blocking reads for | ||
//! terminal input and forwards the data to a Rust channel (which supports | ||
//! timeouts). This approach actually is nicely platform-independent. But | ||
//! terminating the helper thread seems impossible, unless the operating | ||
//! system's `TIOCSTI` ioctl or equivalent can be used to inject a poison | ||
//! value into the input stream. | ||
//! 3. The third approach configures the terminal to time out read operations. | ||
//! Raw and cbreak modes for terminals usually set the `VMIN` pseudo control | ||
//! character to 1 and `VTIME` to 0, which instructs the terminal to block | ||
//! reads until at least one character is available. However, when setting | ||
//! `VMIN` to 0 and `VTIME` to n>0, the terminal times out waiting after | ||
//! n*0.1 seconds. | ||
//! | ||
//! This module implements the third approach because it is simple and robust. | ||
//! Notably, it only requires a couple more changes to the terminal | ||
//! configuration over and above the ones already required for cbreak or raw | ||
//! mode. However, since it effectively polls the terminal, the third approach | ||
//! also has higher CPU overhead. That is mitigated somewhat by the large | ||
//! minimum timeout. Alas, that also puts a hard limit on reactivity. | ||
//! | ||
//! | ||
//! # Sans I/O | ||
//! | ||
//! Prettypretty's Python version follows that community's "batteries included" | ||
//! approach and includes a generally useful [terminal | ||
//! abstraction](https://github.com/apparebit/prettypretty/blob/main/prettypretty/terminal.py). | ||
//! By contrast, the Rust version requires the application to bring its own | ||
//! terminal I/O. The latter approach, commonly called *Sans I/O* is recognized | ||
//! by both [Python](https://sans-io.readthedocs.io) and | ||
//! By contrast, the Rust version provides just enough functionality to query a | ||
//! terminal for its color theme and only on Unix. If applications have more | ||
//! complex needs, they are expected to bring their own terminal I/O. The latter | ||
//! approach, commonly called *Sans I/O*, is recognized by both | ||
//! [Python](https://sans-io.readthedocs.io) and | ||
//! [Rust](https://www.firezone.dev/blog/sans-io) communities as an effective | ||
//! means for coping with asynchronous I/O tainting functions throughout an | ||
//! application (i.e., the function coloring challenge). Its value proposition | ||
//! is simple: If we keep I/O out of library code, we can reuse the library with | ||
//! synchronous and asynchronous I/O. With Sans I/O, library code still needs to | ||
//! implement protocol processing, only now it provides a clean interface for | ||
//! plugging the actual I/O routines. | ||
//! means for building libraries that avoid the function coloring challenge | ||
//! and equally work with synchronous and asynchronous I/O. | ||
mod escape; | ||
mod render; | ||
#[cfg(target_family = "unix")] | ||
mod unix; | ||
|
||
pub use escape::{Action, Control, VtScanner}; | ||
pub use render::render; | ||
#[cfg(target_family = "unix")] | ||
pub use unix::{ | ||
Open, ReadWrite, Start, Terminal, TerminalReader, TerminalWriter, TERMINAL_TIMEOUT, | ||
}; |
Oops, something went wrong.