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 wrapT
in aStateValue<T>
as this is done by theList<T>
itself) - Maps in the form of
Map<T>
(the keys areString
s) - 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 view
s 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
orfalse
- 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:
- top left
- top
- top right
- right
- bottom right
- bottom
- bottom left
- 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 expand
s.
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 expand
s.
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.
right
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