1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269
|
# Architecture
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.
## Crates
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`, and `termbox_simple` are 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
`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`.
#### Dependencies of `tiny`:
| 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 |
### libtiny_client
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 a `Client` by providing a
`ServerInfo`. `Client` then 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 `Client`s.
`Client::new()` also returns a tokio channel receiver for `Event`s. IRC
events are passed to users via this channel. `tiny`'s `conn` module
implements the handler for these events.
The `Client` itself can be used to send messages, joining channels etc.
#### Dependencies of `libtiny_client`:
| Dependency | Used for |
| -------------- | ------------- |
| libtiny_wire | IRC message parsing and generation|
| libtiny_common | The "channel name" type |
### libtiny_tui
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.)
#### Dependencies of `libtiny_tui`:
| 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.) |
### libtiny_logger
Implements logging IRC events (incoming messages, user left/joined etc.) to
user-specified log directory.
#### Dependencies of `libtiny_logger`:
| Dependency | Used for |
| -------------- | ------------- |
| libtiny_common | The "channel name" type |
| libtiny_wire | Filtering out IRC message formatting characters (colors etc.) |
### libtiny_wire
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.
#### Dependencies of `libtiny_wire`:
| Dependency | Used for |
| -------------- | ------------- |
| libtiny_common | The "channel name" type |
### libtiny_common
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.
### term_input
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 `Input`s, 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`.
#### Dependencies of `term_input`:
| Dependency | Used for |
| ----------------- | ------------- |
| term_input_macros | Generating parser for xterm byte sequences |
### term_input_macros
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:
```rust
byte_seq_parser! {
my_parser -> &'static str,
[1, 2, 3] => "first",
[1, 3, 4] => "second",
[2] => "third",
}
```
generates this function:
```rust
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.
### termbox_simple
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.
|