App
The starting point of any good "live" or interactive terminal display is an App
. The App
takes care of generating and updating the visuals as well as taking in user input making use of it (e.g. to update the display accordingly). An app has some content. This content is in the form of AbstractWidget
elements. These widgets are single content elements that serve a specific function, for example displaying some text or acting as buttons etc. More on widgets later. In addition to knowing what is in an app, we also need to specify how it should look like. Specifically, how should the different widgets be layed out. So in addition to creating widgets, you will need to specify a layout for your app. There's a lot more to apps, but for now we can start with some simple examples
A first example
We start with a simple example: an app that only shows one single widget. a TextWidget
displaying a bit of text. As the output is quite long, we supress the it to avoid cluttering this page. Note that Term 2.0 is required to create live widgets.
using Term
using Term.LiveWidgets
app = App(
TextWidget("""
The starting point of any good "live" or interactive terminal display is an `App`. The `App` takes care of generating and updating the visuals as well as taking in user input making use of it (e.g. to update the display accordingly).
An app has some content. This content is in the form of `AbstractWidget` elements. These widgets are single content elements that serve a specific function, for example displaying some text or acting as buttons etc. More on widgets later. In addition to knowing **what** is in an app, we also need to specify **how** it should look like. Specifically, how should the different widgets be layed out. So in addition to creating widgets, you will need to specify a layout for your app.
There's a lot more to apps, but for now we can start with some simple examples
""")
);
╭──────────────────────────────────────╮
│ internals::Term.LiveWidgets.AppI │
│ nternals Term.LiveWidgets.AppInte │
│ rnals(IOBuffer(dat... │
│ measure::Term.Measures.Meas │
│ ure (h: │
│ 24, w: 80) │
│ controls::AbstractDict │
│ Dict('h' => Term.LiveWid │
│ gets.toggle_help,... │
│ parent::Union │
│ │
│ nothing │
│ │
│ │
│ ╲ ╲ ╲ ╲ ╲ ╲ │
│ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲... │
│ │
│ │
│ ╲ ╲ ╲ ╲ ╲ ╲ │
│ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲... │
│ │
│ │
│ ╲ ╲ ╲ ╲ ╲ ╲ │
│ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲... │
│ │
│ │
│ ╲ ╲ ╲ ╲ ╲ ╲ │
│ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲... │
│ │
│ │
│ ╲ ╲ ╲ ╲ ╲ ╲ │
│ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲... │
│ │
│ │
│ ╲ ╲ ╲ ╲ ╲ ╲ │
│ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲... │
│ │
│ │
│ ╲ ╲ ╲ ╲ ╲ ╲ │
│ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲... │
│ │
│ │
│ ╲ ╲ ╲ ╲ ╲ ╲ │
│ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲... │
│ │
│ │
│ ╲ ╲ ╲ ╲ ╲ ╲ │
│ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲... │
│ │
│ │
│ ╲ ╲ ╲ ╲ ╲ ╲ │
│ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲... │
│ │
│ │
│ ╲ ╲ ╲ ╲ ╲ ╲ │
│ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲... │
│ │
│ compositor::Compositor │
│ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ │
│ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ... │
│ │
│ ╲ ╲ ╲ ╲ ╲ ╲ │
│ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲... │
│ │
│ │
│ ╲ ╲ ╲ ╲ ╲ ╲ │
│ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲... │
│ │
│ │
│ ╲ ╲ ╲ ╲ ╲ ╲ │
│ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲... │
│ │
│ │
│ ╲ ╲ ╲ ╲ ╲ ╲ │
│ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲... │
│ │
│ │
│ ╲ ╲ ╲ ╲ ╲ ╲ │
│ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲... │
│ │
│ │
│ ╲ ╲ ╲ ╲ ╲ ╲ │
│ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲... │
│ │
│ │
│ ╲ ╲ ╲ ╲ ╲ ╲ │
│ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲... │
│ │
│ │
│ ╲ ╲ ╲ ╲ ╲ ╲ │
│ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲... │
│ │
│ │
│ ╲ ╲ ╲ ╲ ╲ ╲ │
│ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲... │
│ │
│ │
│ ╲ ╲ ╲ ╲ ╲ ╲ │
│ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲... │
│ │
│ │
│ ╲ ╲ ╲ ╲ ╲ ╲ │
│ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲... │
│ │
│ │
│ ╲ ╲ ╲ ╲ ╲ ╲ │
│ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲ ╲... │
│ │
│ layout::Expr │
│ │
│ A(24, 1.0) │
│ │
│ width::Int64 │
│ │
│ 80 │
│ │
│ height::Int64 │
│ │
│ 24 │
│ │
│ expand::Bool │
│ │
│ true │
│ │
│ widgets::AbstractDict │
│ Dict(:A => Term.LiveWid │
│ gets.TextWidget(Ter... │
│ transition_rules::AbstractD │
│ ict Dict(Term.LiveWidgets │
│ .ArrowDown() =>... │
│ active::Symbol │
│ │
│ A │
│ │
│ on_draw::Union │
│ │
│ nothing │
│ │
│ on_stop::Union │
│ │
│ nothing │
│ │
│ │
│ │
│ │
│ │
╰───────────── Term.LiveWidgets.App ───╯
Easy. Now to use the app you'd call play(app)
. This starts an interactive session in which the app continuously refreshes its display and reacts to user input until the app is exited by pressing q
or Esc
. Unfortunately we can't do that here in the docs, but we can use frame
to see what the app would look like when we start it:
frame(app)
│The starting point of any good "live" or interactive terminal display
│is an `App`. The `App` takes care of generating and updating the visuals
│as well as taking in user input making use of it (e.g. to update the
│display accordingly).
│An app has some content. This content is in the form of `AbstractWidget`
│elements. These widgets are single content elements that serve a specific
│function, for example displaying some text or acting as buttons etc.
│More on widgets later. In addition to knowing **what** is in an app,
│we also need to specify **how** it should look like. Specifically, how
│should the different widgets be layed out. So in addition to creating
│widgets, you will need to specify a layout for your app.
│There's a lot more to apps, but for now we can start with some simple
│examples
│
Press h
while using play
to interact with an app to display a help tooltip.
Layout
Ok, one widget apps are not that useful. When adding more widgets you'll eventually have to specify how to lay them out. Essentially you want to specify the space taken by the app as a whole (the width and height in the terminal) and then within that you need to specify the size of each widgets and where they are located (e.g. widget A is to the left of B and A,B together are above C).
To specify the layout you need to use an Expr
like the one used for 'Compositor' content. The expression is made of elements like a(h, w)
where a
is the layout element's name and h,w
are the height and width of the element. Note that h
has to be an integer (the number of lines spanned by the widget) but w
can be either Int
(number of columns) or Float64
with 0 < w < 1
to specify the fraction of the available space that should be used.
For example:
:r(10, 0.5)
says that the widget r
should take up 10 lines and half of the available width.
In addition to elements, you can use *
and /
to specify the relation between elements: *
means "to the side of" and /
means "above". Combined with parentheses you can get some pretty complex layouts. For example:
layout = :(
:(( a(10, .5) * b(10, .5) )/c(10, 1))
)
╭───────────────────────────────────────────────────────────────╮
│ :((a(10, 0.5) * b(10, 0.5)) / c(10, 1)) │
│ ─────────────────────────────────────────────────────────── │
│ head::Symbol quote │
│ args::Vector Any[:((a(10, 0.5) * b(10, 0.5)) / c(10, 1))] │
│ │
╰────────────────────────────────────────────────────── Expr ───╯
With a layout in mind you can start creating your app. Eventually you'll need to provide some widgets too, but if you just want to check the layout (and maybe tweak it) you can create an empty app with placeholders to visualize the position of each widget:
App(layout) |> frame
Note that you can always specify the width
and height
of the app. If you don't, the app will try to use the full terminal size.
If you use a Float
to specify your layout elements size, the app will automatically resize the elements when the terminal size is reduced. If you also want your app to expand to fill in the whole terminal if the terminal is enlarged, you can use expand
keyword argument for App
.
Adding widgets
To create an app with multiple widgets, you'll need the layout info as shown above and a Dict
with the widgets you want your app to display. The keys in the Dict
need to match the layout elements names. For example, to create an app showing two pieces of text.
layout = :(a(25, .5) * b(25, .5))
widgets = Dict(
:a => TextWidget("""To create an app with multiple widgets, you'll need the layout info as shown above and a `Dict` with
the widgets you want your app to display. The keys in the `Dict` need to match the layout elements names.
For example, to create an app showing two pieces of text.
"""; as_panel=true),
:b => TextWidget("""
The starting point of any good "live" or interactive terminal display is an `App`. The `App` takes care of generating and updating the visuals as well as taking in user input making use of it (e.g. to update the display accordingly).
An app has some content. This content is in the form of `AbstractWidget` elements. These widgets are single content elements that serve a specific function, for example displaying some text or acting as buttons etc. More on widgets later. In addition to knowing **what** is in an app, we also need to specify **how** it should look like. Specifically, how should the different widgets be layed out. So in addition to creating widgets, you will need to specify a layout for your app.
There's a lot more to apps, but for now we can start with some simple examples
"""; as_panel=true)
)
App(layout; widgets=widgets) |> frame
╭──────────────────────────────────────╮╭──────────────────────────────────────╮
│ To create an app with multiple ││ The starting point of any │
│ widgets, you'll need the layout ││ good "live" or interactive termin │
│ info as shown above and a `Dict` ││ al display is an `App`. The │
│ with ││ `App` takes care of generating │
│ the widgets you want your app ││ and updating the visuals as │
│ to display. The keys in the ││ well as taking in user input │
│ `Dict` need to match the layout ││ making use of it (e.g. to update │
│ elements names. ││ the display accordingly). │
│ For example, to create an app ││ An app has some content. This │
│ showing two pieces of text. ││ content is in the form of `Abstra │
│ ││ ctWidget` elements. These widgets │
│ ││ are single content elements │
│ ││ that serve a specific function, │
│ ││ for example displaying some │
│ ││ text or acting as buttons etc. │
│ ││ More on widgets later. In additio │
│ ││ n to knowing **what** is in │
│ ││ an app, we also need to specify │
│ ││ **how** it should look like. │
│ ││ Specifically, how should the │
│ ││ different widgets be layed out. │
│ ││ So in addition to creating │
│ ││ ... content omitted ... │
╰──────────────────────────────────────╯╰──────────────────────────────────────╯
Note that one panel is dim while the other is not, why? That's because the app considers the first widget to be 'active' and the brighter color signals that. Different widgets show that they are active differently, but generally they use colors to signal to the user that they are the currently active one. This is important because user input's (like key presses) will be passed to the currently active widget. For example, if you have a widget that is a button and it get's pressed by using spacebar
, then pressing space will only work if the button is active.
To change the currently active widget you can "navigate" through the app using arrow keys. App
analyzes the layout
of the app to infer the relative position of the widgets and set up the navigation accordingly. To test this, use play
on the app we just created and then left/right arrow to change focus! (Don't forget to use q
to exit the app when you're done)
Activating widgets by keyboard input
Sometimes you want different ways to specify which widget should be active, either by using an arrow key or by pressing a specific key. While there are some defaults for simple apps, you will likely need to specify these "transition rules" manually for most layouts. This is done by passing a Dict
as the keyword argument transition_rules
when you create your App
. The keys of transition_rules
should be of type KeyInput
, and the values should of type Dict
, mapping symbols to symbols. The wording can get complicated with these nested dictionaries, so the example below hopefully explains how to define apropriate transition rules:
App(
# Layout: 3 columns (a b c), with b split in the middle
:(a(10, 0.2) * (b1(5, 0.2) / b2(5, 0.2)) * c(10, 0.2));
widgets = Dict(
:a => TextWidget("Box 1", as_panel=true),
:b1 => TextWidget("Box 2.1", as_panel=true),
:b2 => TextWidget("Box 2.2", as_panel=true),
:c => TextWidget("Box 3", as_panel=true),
), transition_rules = Dict(
ArrowRight() => Dict(:a => :b1, :b1=>:c, :b2=>:c),
ArrowLeft() => Dict(:c => :b1, :b1=>:a, :b2=>:a),
ArrowDown() => Dict(:b1 => :b2),
ArrowUp() => Dict(:b2 => :b1),
)
)
To mentally parse the first transition rule, you should think "When ArrowRight is pressed, if :a is selected, move to :b1. If :b1 is selected, move to :c. If :b2 is selected, move to :c."