This document describes tiny's high-level architecture, the parts that are unlikely to change often, with the rationale behind some of the design decisions. If you are interested in contributing to tiny, this is the best place to start.
tiny consists of 9 crates. The dependencies between these crates are as follows:
Separation to smaller crates was mainly motivated by two things:
-
The crates
libtiny_client,term_input, andtermbox_simpleare reusable outside of tiny. More details are in the individual sections of the crates below. -
Rust allows recursive imports within a crate and over time tiny started to become a big crate where everything depended on everything else. Separation to crates makes separating concerns easier and dependencies between modules/crates and crate interfaces become clear.
Unfortunately this makes it difficult to publish tiny on crates.io (#257). We currently do not publish tiny on crates.io.
Below are the crates in more detail:
tiny is the main crate that provides the tiny executable. It brings
everything together.
- Implements command-line interface (CLI) and command-line argument parsing
- Implements config file parsing
- Initializes loggers (both debug logging and message logging)
- Initializes tokio runtime and creates clients
- Initializes TUI
- Implements updating the TUI on client events (e.g. to show an incoming
message on TUI). This is implemented in module
conn. - Implements updating clients on TUI events (e.g. to send a message when the
user enters a message in the TUI). This is implemented in module
ui.
| Dependency | Used for |
|---|---|
| libtiny_client | Managing IRC connections (maintaining conns, getting incoming msgs, sending msgs, ...) |
| libtiny_tui | Drawing the UI on the terminal |
| libtiny_logger | User and server message logging (not debug logging) |
| libtiny_wire | For IRC message definitions (types), used to handle incoming IRC messages |
| libtiny_common | The "channel name" type |
Provides three key types to create and maintain an IRC connection:
-
ServerInfo: encapsulates information to connect to an IRC server and maintain the connection (server address/port, NickServ/SASL/etc. credentials, nicks, ...). -
Event: an enum type for IRC events ("connected", "message received" etc.) -
Client: the connection handle type. Users create aClientby providing aServerInfo.Clientthen maintains the connection (handles timeouts, disconnects, nick selection and identification etc. everything needed to keep the connection alive).Note that a client maintains one connection. If you want to connect to N servers you need N
Clients.Client::new()also returns a tokio channel receiver forEvents. IRC events are passed to users via this channel.tiny'sconnmodule implements the handler for these events.The
Clientitself can be used to send messages, joining channels etc.
| Dependency | Used for |
|---|---|
| libtiny_wire | IRC message parsing and generation |
| libtiny_common | The "channel name" type |
Handles user input and drawing the terminal UI (TUI). At a high-level the API
is very similar to libtiny_client: on initialization the user passes a config
(file path for the tiny config file), libtiny_tui returns two tokio channel
for user input events and a TUI handle to update the TUI. The types are:
-
Event: Enum for TUI events like a message submitted by the user, or an exit request. -
TUI: The type to modify TUI (create tabs, show messages etc.)
| Dependency | Used for |
|---|---|
| term_input | Input handling (reading events from stdin) |
| termbox_simple | Terminal manipulation (drawing) |
| libtiny_common | The "channel name" type |
| libtiny_wire | Parsing IRC message formatting characters (colors etc.) |
Implements logging IRC events (incoming messages, user left/joined etc.) to user-specified log directory.
| Dependency | Used for |
|---|---|
| libtiny_common | The "channel name" type |
| libtiny_wire | Filtering out IRC message formatting characters (colors etc.) |
Implements IRC message parsing and generation. Entry point for parsing is
parse_irc_msg, which returns at most one Msg, which is the type for IRC
messages.
For message generation, we only have a few functions like privmsg, join
etc. for the messages we need in tiny.
| Dependency | Used for |
|---|---|
| libtiny_common | The "channel name" type |
This crate currently has just one type: ChanName, which is the type for
channel names.
RFC 2812 has two rules related to character names:
- Channel names are case insensitive
- The characters "{}|^" are considered lowercase, and their uppercase equivalents are "[]\~".
There's also a rule implemented by servers:
- For non-ASCII characters the comparison is case sensitive.
The ChanName type provides a newtype with comparison implementation that
follows these rules.
Because channel names are widely used in the implementation of tiny, all other
tiny crates need the ChanName type. To avoid introducing dependencies between
large crates just for one type, we have this crate.
libtiny_common does not depend on other tiny crates.
Parses stdin contents to key events. On initialization sets stdin to
non-blocking mode, to work around a WSL bug (#269). The main type is Input,
which implements Stream (from futures) to allow asynchronously reading
input events.
term_input allows creating multiple Inputs, but you should only have one at
a time.
term_input does not use terminfo. Instead it has hard-coded byte sequences
for xterm key events. Most terminals I tried use xterm byte sequences so this
is mostly fine. However we've seen a case where a recent version of xterm
updated one of its byte sequences, causing tiny to not work properly, see #295.
For efficiently mapping bytes read from stdin to key events term_input
generates a parser from hard-coded xterm byte sequences using
term_input_macros.
| Dependency | Used for |
|---|---|
| term_input_macros | Generating parser for xterm byte sequences |
Provides a single procedural macro called byte_seq_parser. Given a list of
byte array to expression mappings, the macro returns a parser function that
internally uses a decision tree to parse the argument (a byte slice) to a value
by spending O(n) time on the input, where n is the size of the input.
Example:
byte_seq_parser! {
my_parser -> &'static str,
[1, 2, 3] => "first",
[1, 3, 4] => "second",
[2] => "third",
}generates this function:
fn my_parser(buf: &[u8]) -> Option<(&'static str, usize)> {
match buf.get(0usize) {
None => None,
Some(byte) => match byte {
1u8 => match buf.get(1usize) {
None => None,
Some(byte) => match byte {
3u8 => match buf.get(2usize) {
None => None,
Some(byte) => match byte {
4u8 => Some(("second", 3usize)),
_ => None,
},
},
2u8 => match buf.get(2usize) {
None => None,
Some(byte) => match byte {
3u8 => Some(("first", 3usize)),
_ => None,
},
},
_ => None,
},
},
2u8 => Some(("third", 1usize)),
_ => None,
},
}
}Second return value is the number of bytes consumed.
term_input_macros does not depend on other tiny crates.
Implements drawing to terminal and suspend/activate functions to temporarily release the terminal (by resetting the attributes) while still allowing users to update the internal buffer.
The main type is Termbox which allows updating cells on the terminal and
drawing the updates to the terminal. Internally the terminal is just a grid of
"cells", where a cell has a character, background color, and foreground color.
Character attributes like "underline" or "bold" are applied to the foreground
color.
After manipulating the internal buffer with change_cell, updates are rendered
with present. When Termbox is suspended with suspend, present doesn't
do anything. change_cell calls still update the internal buffer so after
activate the most recent changes are shown.
termbox_simple tries to be efficient by avoiding updates for cells on the
terminal that are not changed. This is done by comparing contents of the back
buffer (updates done to the last rendered buffer) and the front buffer
(currently drawn stuff). present, after updating the terminal, moves the back
buffer to front buffer.
suspend and activate are used to implement "edit in $EDITOR" function of
tiny, where when the user types C-x (or pastes a multi-line text) and tiny
runs $EDITOR (if the variable is set) with the contents of the input field as
the editor's buffer contents. On exit activate called to show tiny again.
termbox_simple does not depend on other tiny crates.
