Introduction

Anathema is a library for building text user interfaces.

This guide is for the current main branch: https://github.com/togglebyte/anathema

Anathema was created with the intent to give developers a fast and easy way to write text user interfaces (TUI) and ship the template(s) along with the application giving the end user the means and freedom to customise the layout.

Why

  • Templates can be shipped with the application, giving the end user the ability to customise the entire layout of the application.
  • Easy to prototype with the markup language and runtime.

Alternatives

Getting started

Install

Add anathema to your Cargo.toml file.

...
[dependencies]
anathema = { git = "https://github.com/togglebyte/anathema" }

Note

This guide is written against the current version on Github. Even though efforts are made to keep this guide up to date there are possibilities of changes being made and published before they reach this guide.

At the time of writing, Anathema should be considered alpha.

A basic example

Render a border around the text, placing the text in the middle of the terminal.

// src/main.rs
use std::fs::read_to_string;

use anathema::runtime::Runtime;
use anathema::vm::Templates;

fn main() {
    // Step one: Load and compile templates
    let template = read_to_string("templates/index.aml").unwrap();
    let mut templates = Templates::new(template, ());
    let templates = templates.compile().unwrap();

    // Step two: Runtime
    let runtime = Runtime::new(&templates).unwrap();

    // Step three: start the runtime
    runtime.run().unwrap();
}
// templates/index.aml
alignment [align : "center"]
    border [foreground: "cyan"]
        text [foreground: #fa0] "Hello world"

Runtime

#![allow(unused)]
fn main() {
use anathema::runtime::Runtime;

let mut runtime = Runtime::new(&templates).unwrap();
runtime.enable_alt_screen = false;
runtime.fps = 12;
runtime.enable_tabindex = false;
}

Configuring the runtime

The following settings are available:

enable_mouse

Default: false

Enable mouse support in the terminal (if it's supported).

enable_alt_screen

Default: true

Creates an alternate screen for rendering. Restores the previous content of the terminal to the previous state when the runtime ends.

enable_ctrlc

Default: true

If this is disabled then pressing control + c does nothing.

enable_tabindex

Default: true

Give a view a tabindex value to be able to tab to the view. Read more about tabindices under Views.

fps

Default: 30

The number of frames to render per second.

View

A view exposes event handling and passes any internal state to the runtime. Any type that implements the anathema::core::View trait is a view.

This trait has no required methods, however note that without implementing the fn state(&self) -> &dyn State method, no internal state will be exposed to the template even if the view has a state.

See State for more information about state.

Example

#![allow(unused)]
fn main() {
use anathema::core::{Event, KeyCode, Nodes, View};

impl View for MyView {
    fn on_event(&mut self, event: Event, _: &mut Nodes<'_>) -> Event {
        match event {
            Event::KeyPress(KeyCode::Char(c), ..) => {
            }
            Event::KeyPress(KeyCode::Backspace, ..) => {
            }
            _ => {}
        }
        
        event
    }

    fn state(&self) -> &dyn State {
        &self.state
    }

    fn tick(&mut self) {
        *self.state.counter += 1;
    }

    fn focus(&mut self) {
        *self.state.color = Color::Reset;
    }

    fn blur(&mut self) {
        *self.state.color = Color::Magenta;
    }
    
}
}

Root view

The runtime needs at least one view, however since the View trait is implemented for the unit type it is possible to pass () as an argument instead of your own defined view:

#![allow(unused)]
fn main() {
let view = ();

let template = read_to_string("templates/index.aml").unwrap();

// Pass associate the template with the root view:
let mut templates = Templates::new(template, view);

let templates = templates.compile().unwrap();
let runtime = Runtime::new(&templates).unwrap();
}

This is suitable if no event handling or no internal state is required.

One or many views

Singular instance of a view

If the number of instances are known at compile time, for instance the view is not created inside a for-loop, then use add_view to register a view with a template.

#![allow(unused)]
fn main() {
let items_view = ItemsView::new();

let items_template = read_to_string("templates/index.aml").unwrap();
templates.add_view(
    "items",        // Name of the view in the template
    items_template,  // Template
    items_view       // A view instance
);
}

This is then accessible in the template with the view syntax:

@items

Multiple / dynamic instances of a view

If a view is used inside a for-loop or is otherwise generated as requested by the runtime use add_prototype:

#![allow(unused)]
fn main() {
let item_view = ;

let item_template = read_to_string("templates/index.aml").unwrap();
templates.add_prototype(
    "item",             // Name of the view in the template
    item_template,      // Template
    || ItemView::new()  // A closure creating a view instance when called
);
}

This makes it possible to create views on the fly:

for item in items
    @item item

State

Example

Example of internal state

#![allow(unused)]
fn main() {
#[derive(State)]
struct MyState {
    name: StateValue<String>,
    guests: List<String>,
}

struct MyView(MyState);

impl View for MyView {
    fn state(&self) -> &dyn State {
        &self.0
    }
}

}

There are two types of state:

  • Internal
  • External

External state

External state is state that is passed to the view from the outside.

A state can either be a direct state from a parent view:

// Anathema template
@chid parent_state

or a custom map declared in the template:

// Anathema template
@myview {"bunnies": list_of_bunnies}

Internal state

Internal state is provided by the View, by implementing the fn state(&self) -> &dyn State method from the View trait.

The state is owned by the view, and the view has mutable access to the state.

A state can contain:

  • Primitives, wrapped in StateValue<T>
  • Collections in the form of List<T> (It's redundant to wrap T in a StateValue<T> as this is done by the List<T> itself)
  • Maps in the form of Map<T> (the keys are Strings)
  • Any other type that implements State

A state has to be a struct with named fields.

Example of a nested internal state

#![allow(unused)]
fn main() {
#[derive(State)]
struct Outer {
    name: StateValue<String>,
    collection: List<String>,
    dict: Map<usize>,
    nested_state: Inner
    many_inners: List<Inner>
}

#[derive(State)]
struct Inner {
    counter: StateValue<i32>
}

impl View for Index {
    fn tick(&mut self) {
        *self.state.nested_state.counter += 1;
    }

}
}

Making a change to the wrapped value of StateValue<T> is what causes the update of the widgets, therefore the StateValue<T> should not be replaced, but rather changes should be made to the inner value via deref mut:

#![allow(unused)]
fn main() {
*self.state += 1;
}

Templates

Anathema has a template language to describe user interfaces. This makes it possible to ship the template(s) together with the compiled binary so the application can be customised without having to be recompiled.

The widget syntax looks as follows: <widget> <attributes> <value>

  Widget name                                         
  |    Start of (optional) attributes
  |    |    Attribute name                            
  |    |    |          Attribute value
  |    |    |          |     Attribute separator      End of attributes
  |    |    |          |     |                        | Values / Text
  |    |    |          |     |                        | |
  |    |    |          |     |                        | |------+-+-+
  |    |    |          |     |                        | |      | | |
widget [optional: "attribute", separated: "by a comma"] "text" 1 2 ident

Widgets don't receive events (only views does) and should be thought of as something that is drawn to the screen.

There is for instance no "input" widget. To represent an input widget the view would handle the key press, capture the char and write it to a state value, and in the template an input field could be written as text input_value.

Attributes

Widget attributes are optional.

Attributes consists of a collection of ident, :, and value (more about values in the next section).

An ident has to start with a letter (a-zA-Z) and can contain one or more _.

Example:

widget [attribute_a: "some string", attribute_b: false]

An attribute name is always an ident, however the value can be anything.

Values

A value can be one of the following:

  • string: "Empty vessel, under the sun"
  • integer: 123
  • float: 1.23
  • colour: #fab or #ffaabb
  • boolean: true or false
  • list: [1, 2, 3]
  • map: {"key": "value"}
  • ident: state_value

Idents are used to access state values.

Widgets and children

Some widgets can have one or more children.

Example: a border widget with a text widget inside

border
    text "look, a border"

Loops

Given a collection it's possible to loop over the elements of the collection. The syntax for loops are for <element> in <collection>.

Example: looping over static values

for value in [1, 2, "hello", 3]
    text value

Widgets generated by the loop can be thought of as belonging to the parent widget as the loop it self is not a widget.

Example: mixing loops and widgets

vstack
    text "start"
    for val in [1, 2, 3]
        text "some value: " val "."
    text "end"

The above example would render widgets as:

vstack
    text "start"
    text "some value: " 1 "."
    text "some value: " 2 "."
    text "some value: " 3 "."
    text "end"

Note: For-loops does not work inside attributes, and can only produce widgets.

If / Else

It's possible to use if and else to determine what to layout.

Example:

if true
    text "It's true!"
    
if value > 10
    text "Larger than ten"
else if value > 5
    text "Larger than five but less than ten"
else
    text "It's a small value..."

Operators

  • Equality ==
  • Greater than >
  • Greater than or equals >=
  • Less than <
  • Less than or equals <=
  • And &&
  • Or ||

Note: just like for-loops it's not possible to use if/else with attributes.

Widgets

Even though it's possible to create your own widgets Anathema aims to provide the necessary building blocks to represent almost any layout.

Widgets that draw them selves (e.g text, span and border) support these default attributes:

foreground

Foreground colour

Valid values:

  • hex: #ffaabb
  • string: "green"

background

Background colour (see foreground for valid values)

bold

Valid values: true or false

italic

Valid values: true or false

Default widgets

The following is a list of available widgets and their template names:

  • Text (template name: text)
  • Span (template name: span)
  • Border (template name: border)
  • Alignment (template name: alignment)
  • VStack (template name: vstack)
  • HStack (template name: hstack)
  • ZStack (template name: zstack)
  • Expand (template name: expand)
  • Spacer (template name: spacer)
  • Position (template name: position)
  • Viewport (template name: viewport)

Text (text)

Print text.

To add styles to text in the middle of a string use a span widget.

Example

text [foreground: "red"] "I'm a little sausage"

Attributes

wrap

Default is to wrap on word boundaries such as space and hyphen, and this method of wrapping will be used if no wrap attribute is given.

Valid values:

  • "overflow": the text is truncated when it can no longer fit
  • "break": the text will wrap once it can no longer fit

Squash

Squash will ignore any white-space that would be written on a singular line. Default is true

Example with squash set to true:

border [width: 5 + 2]
    text "hello world"
┌─────┐
│hello│
│world│
└─────┘

Example with squash set to false:

border [width: 5 + 2]
    text [squash: false] "hello world"
┌─────┐
│hello│
│     │
│world│
└─────┘

text-align

Note that text align will align the text within the text widget. The text widget will size it self according to its constraint.

To right align a text to the right side of the screen therefore requires the use of the alignment widget in combination with the text align attribute.

Default: left

Valid values:

  • "left"
  • "right"
  • "centre" | "center"

Example of right aligned text

border [width: 5 + 2]
    text [text-align: "right"] "hello you"
┌─────┐
│hello│
│  you│
└─────┘

Example of centre aligned text

border [width: 5 + 2]
    text [text-align: "centre"] "hello you"
┌─────┐
│hello│
│ you │
└─────┘

Additional styles

  • bold
  • italic
text [bold: true, italic: true] "bold AND italic?!"

Span (span)

The span widget is used to style text on the same line. Span can not be used on its own, it has to belong to a text widget.

text "start"
    span "-middle-"
    span "end"
start-middle-end

The span widget can be styled using the standard attributes, such as bold, foreground etc.

Border (border)

Example

border
    text "What a border!"
┌──────────────┐
│What a border!│
└──────────────┘

Attributes

width

The fixed width of the border

height

The fixed height of the border

min-width

The minimum width of the border

min-height

The minimum height of the border

sides

Valid values:

  • "top"
  • "right"
  • "bottom"
  • "left"

Sides specifies which sides of the border should be drawn.

This can be written as an individual side:

border [sides: "left"]
    text "What a border!"
│What a border!

or

border [sides: "left"]
    text "What a border!"
──────────────
What a border!

It can also be written as a list:

border [sides: ["left", "top"]]
    text "What a border!"
┌──────────────
│What a border!

border-style

The default border style is "thin".

For a thicker border style the value "tick" can be used:

border [border-style: "thick"]
    text  "What a border!"
╔══════════════╗
║What a border!║
╚══════════════╝

It's also possible to customise the border style by providing an eight character string as input, where each character represents a portion of the corner in the following order:

  1. top left
  2. top
  3. top right
  4. right
  5. bottom right
  6. bottom
  7. bottom left
  8. left

See the following example:

border [border-style: "12345678"]
    text  "What a border!"
1222222222222223
8What a border!4
7666666666666665

Alignment (alignment)

Alignment will inflate the wrapping widget if any align value is given other than "top-left".

Example

border [width: 16, height: 5]
    alignment [align: "centre"]
        text "centre"
┌──────────────┐
│              │
│    centre    │
│              │
└──────────────┘

Attributes

align

Valid values:

  • "top-left"
  • "top"
  • "top-right"
  • "right"
  • "bottom-right"
  • "bottom"
  • "bottom-left"
  • "left"
  • "centre" or "center"

VStack (vstack)

Vertically stack widgets

Example

vstack
    text "one"
    text "two"
one
two

Attributes

width

Fixed width

height

Fixed height

min-width

Minimum width

min-height

Minimum height

HStack (hstack)

Horizontally stack widgets

Example

hstack
    text "one"
    text "two"
onetwo

Attributes

width

Fixed width

height

Fixed height

min-width

Minimum width

min-height

Minimum height

ZStack (zstack)

Stack widgets on top of each other

Example

zstack
    text "333"
    text "22"
    text "1
123

Attributes

width

Fixed width

height

Fixed height

min-width

Minimum width

min-height

Minimum height

Expand (expand)

Expand the widget to fill the remaining space.

The layout process works as follows: First all widgets that are not expand or spacer will be laid out. The remaining space will be distributed between expand then spacer.

The size is distributed evenly between all expands.

To alter the distribution factor set the factor attribute.

Example

border [width: 10, height: 11]
    vstack
        expand
            border
                expand
                    text "top"
        expand
            border
                expand
                    text "bottom"
        text "footer"
┌────────┐
│┌──────┐│
││top   ││
││      ││
│└──────┘│
│┌──────┐│
││bottom││
││      ││
│└──────┘│
│footer  │
└────────┘

Attributes

factor

The factor decides the amount of space to distribute between the expands.

Given a height of 33 and two expand widgets, the height would be divided by two.

If one of the expand widgets had a factor of two, then it would receive 22 of the total 33 height, and the remaining widget would receive 11.

axis

Expand along an axis.

Valid values:

  • "horz" | "horizontal"
  • "vert" | "vertical"

fill

Fill the unpainted space with a string.

Example:

border [width: 10, height: 5]
    expand [fill: "+-"]
        text "Hello"
┌────────┐
│Hello-+-│
│+-+-+-+-│
│+-+-+-+-│
└────────┘

Position (position)

Position the widget relative to its parent.

Example

border [width: 10, height: 5]
    position [top: 1, left: 1]
        text "Hi"
┌────────┐
│        │
│ Hi     │
│        │
└────────┘

To position a widget on top of another widget combine the position widget with a zstack:

zstack
    border [width: 10, height: 5]
    position [top: 0, left: 2]
        text "] Hi ["
┌─] Hi [─┐
│        │
│        │
│        │
└────────┘

Attributes

left

Position the left side of the widget with an offset of the given value.

Position the right side of the widget with an offset of the given value.

border [width: 10, height: 5]
    position [top: 0, right: 0]
        text "Hi"
┌────────┐
│      Hi│
│        │
│        │
└────────┘

top

Position the top side of the widget with an offset of the given value.

bottom

Position the bottom side of the widget with an offset of the given value.

Spacer (spacer)

The spacer widget will fill out any remaining space within a widget.

Example

border
    hstack
        text "Hi"
        spacer
┌─────────────────────────┐
│Hi                       │
└─────────────────────────┘

without the spacer:

border
    hstack
        text "Hi"
┌──┐
│Hi│
└──┘

Viewport (viewport)

The viewport allows the widgets to overflow in a given direction.

It's important to note that the viewport is an unbounded widget. This means that widgets can be laid out indefinitely along a given axis.

Example

border [height: 4, width: 10]
    viewport [offset: 2]
        text "1"
        text "2"
        text "3"
        text "4"
┌────────┐
│3       │
│4       │
└────────┘

Attributes

direction

Specifies the direction to lay out the widgets.

Default value: "forwards"

Valid values:

  • "backwards" or "backward"
  • "forwards" or "forward"
border [height: 5, width: 10]
    viewport [direction: "backward"]
        text "1"
┌────────┐
│        │
│        │
│1       │
└────────┘

axis

Specify along which axis to layout the widgets.

Valid values:

  • "horz" | "horizontal"
  • "vert" | "vertical"

offset

Offset the widgets. See the example at the top.

clamp

Clamp the offset preventing the last widget from being drawn outside of the viewport.

Note: this feature is currently broken