Zi is a library for building modern terminal user interfaces.
A user interface in Zi is built as a tree of stateful components. Components
let you split the UI into independent, reusable pieces, and think about each
piece in isolation.
The [`App`](app/struct.App.html) runtime keeps track of components as they are
mounted, updated and eventually removed and only calls `view()` on those UI
components that have changed and have to be re-rendered. Lower level and
independent of the components, the terminal backend will incrementally
redraw only those parts of the screen that have changed.
# A Basic Example
The following is a complete example of a Zi application which implements a
counter. It should provide a good sample of the different
[`Component`](trait.Component.html) methods and how they fit together.
A slightly more complex version which includes styling can be found at
`examples/counter.rs`.
![zi-counter-example](https://user-images.githubusercontent.com/797170/137802270-0a4a50af-1fd5-473f-a52c-9d3a107809d0.gif)
Anyone familiar with Yew, Elm or React + Redux should be familiar with all
the high-level concepts. Moreover, the names of some types and functions are
the same as in `Yew`.
```rust
use zi::{
components::{
border::{Border, BorderProperties},
text::{Text, TextAlign, TextProperties},
},
prelude::*,
};
use zi_term::Result;
// Message type handled by the `Counter` component.
enum Message {
Increment,
Decrement,
}
// The `Counter` component.
struct Counter {
// The state of the component -- the current value of the counter.
count: usize,
// A `ComponentLink` allows us to send messages to the component in reaction
// to user input as well as to gracefully exit.
link: ComponentLink<Self>,
}
// Components implement the `Component` trait and are the building blocks of the
// UI in Zi. The trait describes stateful components and their lifecycle.
impl Component for Counter {
// Messages are used to make components dynamic and interactive. For simple
// or pure components, this will be `()`. Complex, stateful components will
// typically use an enum to declare multiple Message types. In this case, we
// will emit two kinds of message (`Increment` or `Decrement`) in reaction
// to user input.
type Message = Message;
// Properties are the inputs to a Component passed in by their parent.
type Properties = ();
// Creates ("mounts") a new `Counter` component.
fn create(
_properties: Self::Properties,
_frame: Rect,
link: ComponentLink<Self>,
) -> Self {
Self { count: 0, link }
}
// Returns the current visual layout of the component.
// - The `Border` component wraps a component and draws a border around it.
// - The `Text` component displays some text.
fn view(&self) -> Layout {
Border::with(BorderProperties::new(Text::with(
TextProperties::new()
.align(TextAlign::Centre)
.content(format!("Counter: {}", self.count)),
)))
}
// Components handle messages in their `update` method and commonly use this
// method to update their state and (optionally) re-render themselves.
fn update(&mut self, message: Self::Message) -> ShouldRender {
self.count = match message {
Message::Increment => self.count.saturating_add(1),
Message::Decrement => self.count.saturating_sub(1),
};
ShouldRender::Yes
}
// Updates the key bindings of the component.
//
// This method will be called after the component lifecycle methods. It is
// used to specify how to react in response to keyboard events, typically
// by sending a message.
fn bindings(&self, bindings: &mut Bindings<Self>) {
// If we already initialised the bindings, nothing to do -- they never
// change in this example
if !bindings.is_empty() {
return;
}
// Set focus to `true` in order to react to key presses
bindings.set_focus(true);
// Increment
bindings.add("increment", [Key::Char('+')], || Message::Increment);
bindings.add("increment", [Key::Char('=')], || Message::Increment);
// Decrement
bindings.add("decrement", [Key::Char('-')], || Message::Decrement);
// Exit
bindings.add("exit", [Key::Ctrl('c')], |this: &Self| this.link.exit());
bindings.add("exit", [Key::Esc], |this: &Self| this.link.exit());
}
}
fn main() -> zi_term::Result<()> {
zi_term::incremental()?.run_event_loop(Counter::with(()))
}
```
More examples can be found in the `examples` directory of the git
repository.
# License
This project is licensed under either of
* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
http://www.apache.org/licenses/LICENSE-2.0)
* MIT license ([LICENSE-MIT](LICENSE-MIT) or
http://opensource.org/licenses/MIT)
at your option.
### Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion by you, as defined in the Apache-2.0 license, shall be dual
licensed as above, without any additional terms or conditions.