A cross-platform, interoperable, simplfied terminal library that is meant to be wrapped by multiple languages.
tui • tty \ˈtwē-dē \ n. - text-based user interface library for teletype writers (aka. terminals)
NOTE: (Nov. 8, 2019) - This library is still in alpha stage and the API is still in flux. However, the core concepts and goals outlined below will remain fairly stable. Contributions are most welcome! 😄
- Features
- Rationale
- Definitions
- Getting Started
- Aspirations, but not Guarantees
- Contributing
- Versioning
- Authors
- License
- Shoutouts
- Cross-platform (Linux, Mac, Windows)
- Focused (read: small) API footprint - unified, consistent capabilities across terminals
- avoid leaky abstractions that force you to think about what may or may not work
- prefer to keep dependencies limited (Unix: libc, Windows: winapi) and avoid including the kitchen sink
- Thread-safe (guarantees provided by Rust's Send + Sync traits)
- Cursor navigation - eg. goto(col, row), move up/down/left/right
- Screen manipulations - eg. resize, clear, print, enter/leave alternate screen
- Styling output - eg. fg and bg colors, bold, dim, underline
- Terminal settings - eg. raw/cooked, hide/show cursor, mouse on/off
- User input handling - eg. keyboard/mouse events
- Minimal memory
unsafe
code: only OS specific calls and FFI which follows the Rust FFI Nomicon very closely
-
Why not use curses?
Show response
While [n/pd]curses is widely used and wrapped, there is also plenty issues regarding them: wide character support, cross-platform support, C-style/low-level imports that reduce clarity, etc.
-
Why not use blessings (Python), tty-tk (Ruby), terminal-kit (Node), or insert project (insert language)?
Show response
As you can see, there is already a proliferation of various implementations of terminal libraries...and yes I'm aware of the irony that this project is +:one: to the list of implementations out there.
However, unlike other attempts, what this project intends to do is to create a unifying API across languages that eliminates the need to repeat yourself. This is actually very similar to how asdf-vm addressed the proliferation of "version managers" like
rbenv
,gvm
,nvm
, andpyenv
. By creating something unifying and extensible, users won't have to re-discover and re-learn a new API every time they switch programming languages.Additionally, many of the implementations out there do not provide cross-platform support (mainly Windows Console), which I'm specifically targeting with this project.
-
Why the command line? Why cross-platform? Why, why, why?!
Show response
At the end of the day, many development workflows begin and end with a terminal prompt. I wanted to learn and better understand this critical component of a software engineer's journey. Consequently, this process has gotten me familiar with systems programming languages (Rust, Go, C, and Nim), low-level OS syscalls, the Windows Console API, and countless other intangibles that have made me a more well-rounded individual.
Cross-platform
Expand description
- Needs to consistently work on MacOS, Linux, and Windows
- BSDs and others would be secondary
- Needs to work on these architectures:
- ARM - 32/64-bit
- Intel - 32/64-bit
- AMD - 32/64-bit
Interoperable
Expand description
- Needs to be portable to multiple languages (ones that have an FFI with C)
- C has too many ⏳💣💥 so such interoperability is provided by Rust (maybe Nim)
Simplified
Expand description
- Basic functionality scoped to the below:
- Cursor actions (motion)
- Screen actions (printing/clearing)
- Output actions (styling)
- Term mode actions (raw/cooked)
- Input event handling
- Implemented with as little "in the middle" as possible
- Tight scoping allows us to focus on specific elements to optimize performance rather than peanut-buttering across too many concerns
- Being clear > being clever
- Rust actually provides great options for abstractions (eg. Traits, macros) but these should be carefully considered over a more straight-forward method—even if they are more idiomatic Rust. Often, traits and macros make code less understandable for newcomers as they can be/get quite "magical".
- The analogy that comes to mind is that, for the longest time, Go(lang) did not want to provide generics because the feeling was that they reduced readability and made the language more complex. Instead, the tradeoff made was that some repetition was more beneficial towards maintainable code than bluntly trying to be DRY. Likewise, to keep things simplified, I'd rather repeat things that make what is going on obvious and less opaque.
tuitty's architectural design attempts to mirror reality. There are actually two (2) feedback loops happening when an application begins:
-
The "outer loop": a User receives visual cues from the terminal and, in response, does things that emits input events (eg. pressing keys on a keyboard), which in turn does stuff to the terminal, and
-
The "inner loop": the Application receives a signal or request, processes or fetches application state/data accordingly, updates the application state, and performs operations to the view, which causes the "stuff" to be done to the view that provides the visual cue to the User.
Bear in mind, it's just a loop!
The mental model to bear in mind is similar to the Flux pattern for React.js popularized by Facebook.
Phase 1: Receiving an Input Event from the User
The Dispatcher
replicates the parsed InputEvent
and sends it to each listening Event Handle
.
Phase 2: App Requests some internal state
For example, a Signal
was received to get the character underneath the cursor. This requires a Request
made to the Dispatcher
to fetch the cursor position and the character at the corresponding location in the internal screen buffer.
Phase 3: App Signals an appropriate Action to be taken
Perhaps, you want to take the character at position and print it somewhere else on the screen, like a copy + paste
operation.
After the terminal updates, the User will receive that visual cue and provide more inputs for the cycle to start over again.
Is this really a big deal?
These separate diagrams were meant to help build a mental model regarding how the internals of the library work. It is helpful to understand that the Dispatcher
is responsible for sending and receiving Signal
or Request
messages that either does stuff (signal actions) or fetches stuff (request app state). This uses channels under the hood.
This is important, because on Unix systems, in order to parse user input, you would have to read stdin
. But that would be a blocking call. If you wanted to run things concurrently (eg. autocomplete, syntax checking, etc), you would have to read things asynchronously through a spawned thread. It would be impractical to spawn a thread every time you wanted a concurrent process to read from stdin
. Also, why would you need more than a single process reading and parsing from stdin
? Instead of a thread, this implementation creates a new channel that receives InputEvent
s from a single reader of stdin
that is within the Dispatcher
.
Similarly, if you wanted to take actions on the terminal, in the previous paradigm, terminal actions were methods with an object that also held some mutable state (eg. screen buffers, multiple screen contexts, etc). It wasn't clear how that would cross the FFI boundary when attempting multi-threaded or async/await event loops in other languages. Passing a mutable Box<T>
(heap allocated chunk of memory) seemed like a bad idea. However, with this pattern in a similar manner, multiple entities can send Signal
s and make Request
s to the Dispatcher
to be handled safely.
Like I mentioned previously, this is not a pattern that was invented for this particular library. Rather, this pattern pulled inspiration from reactive programming (Rx.js), the actor model / concurrency via message passing (Kafka, Erlang), and web frameworks like Elm, React.js (aforementioned Flux), and re-frame. Actually, the documentation for re-frame has a similar diagram: (see right). The relevant parts are mainly 1-5 since the web stuff is irrelevant here. But notice how similar the flows are to each other. It has been well-documented and proven how these patterns reduce compexity and errors and improve maintainability and speed of development.
- Windows 10 - Cmd.exe (legacy and modern modes)
- Windows 10 - PowerShell (legacy and modern modes)
- Windows 10 - git-bash (w/ winpty)
- Ubuntu 17.04 - gnome-terminal
Expand description
- High performance (can't expect it all to be there as a v1)
- Work flawlessly on all platforms, all architectures, etc. (this is non-trivial)
- Cover all world languages and keyboard layouts (unicode is hard)
- Match idomatic paradigms across programming languages (eager to adopt the best from each)
- Have feature X from this other library Y (eager to evaluate and learn from)
- Completeness (not always is the terminal the best tool for the job; we won't force a square peg into a round hole)
Please read CONTRIBUTING.md for details on our code of conduct, and the process for submitting pull requests to us.
Specifically, there are labels created for each of these areas:
- unicode language support
- unicode emoji support
- interop os/arch support (bsds, arm, amd) 32/64-bit
- performance performance
- rust-stdlib migrations (Futures, Streams)
- ffi-ports ports (Ruby, Python, NodeJS, etc)
- ergonomics ergonomics without being overly clever
We use SemVer(-ish) for versioning. For the versions available, see the TBD
- imdaveho - Creator and project maintainer (profile)
This project is licensed under the MIT License - see the LICENSE.md file for details
nanos gigantum humeris insidentes
Many thanks to the authors and projects below for various implementations that have inspired this project.