Tutorial: Creating an App¶
This page will provide a brief tutorial on creating a new PVTUI application. It is assumed you have already downloaded and compiled the PVTUI library. If not, (see Building from Source). If you choose to use CMake for your application, a minimal CMakeLists.txt file is given below:
cmake_minimum_required(VERSION 3.22)
project(pvtui-example LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# Find pvtui
# Unless pvtui is installed in a standard location (e.g. /usr/lib)
# you'll need to set -Dpvtui_DIR=/path/to/lib/cmake/pvtui
find_package(pvtui REQUIRED)
# Add exectuble and link libraries
add_executable(pvtui-example main.cpp)
target_link_libraries(pvtui-example PRIVATE pvtui::pvtui)
In this tutorial we will create a TUI for the following EPICS database:
record(ao, "$(P)Value") {
field(DESC, "Tutorial!")
field(VAL, 0.0)
}
record(ao, "$(P)TweakVal") {
field(VAL, 1.0)
}
record(calcout, "$(P)TweakFwd") {
field(INPA, "$(P)Value.VAL")
field(INPB, "$(P)TweakVal.VAL")
field(CALC, "A + B")
field(OUT, "$(P)Value.VAL PP")
}
record(calcout, "$(P)TweakRev") {
field(INPA, "$(P)Value.VAL")
field(INPB, "$(P)TweakVal.VAL")
field(CALC, "A - B")
field(OUT, "$(P)Value.VAL PP")
}
For this tutorial we will create a simple UI which has buttons to trigger the TweakFwd and TweakRev records as well as an input for the Value.VAL and Value.DESC. Lets start by instantiating the widgets we will need.
We will start with the below code, which is a good starting point for most applications.
#include <pvtui/pvtui.hpp>
#include <ftxui/component/component.hpp>
using namespace ftxui;
using namespace pvtui;
int main(int argc, char *argv[]) {
// Instantiate an App to manage everything
pvtui::App app(argc, argv);
// Get positional arguments (the prefix, PV names, etc.)
auto pos_args = app.args.positional_args();
if (pos_args.size() < 2) {
printf("Usage: tutorial <prefix>\n");
return EXIT_FAILURE;
}
const std::string prefix = pos_args[1];
// Create all the widgets we need
// InputWidget desc(app, prefix + "pvname.DESC", PVPutType::String);
// ftxui container to define interactivity of components
auto main_container = Container::Vertical({
// include all the interactive widgets here
// desc.component(),
});
// ftxui renderer defines the visual layout
auto main_renderer = Renderer(main_container, [&] {
// define the visual layout of the application
// return hbox({ // e.g.
// desc.component()->Render()
// })
});
// Run the main loop
app.run(main_renderer);
}
The above should apply to most basic TUIs. The prefix is taken as a positional argument and prepended to each PV name.
First we will define the widgets we need.
InputWidget desc(app, prefix + "Value.DESC", PVPutType::String);
InputWidget val(app, prefix + "Value.VAL", PVPutType::Double);
InputWidget twv(app, prefix + "TweakVal.VAL", PVPutType::Double);
ButtonWidget twf(app, prefix + "TweakFwd.PROC", " + ");
ButtonWidget twr(app, prefix + "TweakRev.PROC", " - ");
Any widget that a user can interact with must be added to a ftxui::Container, so we’ll do that now. Don’t worry too much about this. For most PVTUI apps, simply putting all the components in a Container::Vertical is all you need to do.
// ftxui container to define interactivity of components
auto main_container = Container::Vertical({
desc.component(),
val.component(),
twf.component(),
twr.component(),
twv.component(),
});
Everything up to this point can mostly be considered to be boilerplate. Now we will do the hard part of defining the visual layout of the TUI. Some may find the syntax a bit different at first glace, but it is very nice when you get used to it. For more details on this part, consult the FTXUI documentation and example code.
// ftxui renderer defines the visual layout
auto main_renderer = ftxui::Renderer(main_container, [&] {
return vbox({
hbox({
text("DESC: "),
desc.component()->Render()
| size(WIDTH, EQUAL, 15)
| EPICSColor::edit(desc),
}),
separator(),
hbox({
text("VAL: "),
val.component()->Render()
| size(WIDTH, EQUAL, 11)
| EPICSColor::edit(val),
}),
separatorEmpty(),
hbox({
twr.component()->Render(),
separatorEmpty(),
twv.component()->Render()
| size(WIDTH, EQUAL, 5)
| EPICSColor::edit(twv),
separatorEmpty(),
twf.component()->Render(),
})
}) | size(WIDTH, EQUAL, 20);
});
Looking through the above code, you’ll see we create a top level vbox (“vertical box”) with hbox’s (“horizontal box”) inside.
For each widget we call its Render() function then apply styles to it with the | operator. You’ll also notice the
EPICSColor namespace which defines some convenience functions for applying standard color schemes which also change if
connection to the PV is lost. Following the style of MEDM, widgets with EPICSColor will be rendered as white for both the
foreground and background if the underlying PV is disconnected. After defining the renderer, call app.run(main_renderer) to run the main application loop.
Load the test database in an IOC with a P macro of your choosing, e.g. softIoc -m "P=MyIoc:" -d test.db.
Then compile and run the tutorial application: ./tutorial MyIoc:
You should have a PVTUI application as in the screenshot below. Clicking plus or minus will add/subtract the TweakVal to Value.
and if the PVs are not connected, it will look like this:
That’s it. Below the full code is provided (also located at tests/tutorial.cpp).
At this point you might still be missing some details needed to make more complex applications. The best way to
learn is by looking through the provided applications here, and also in the FTXUI repository.
#include <pvtui/pvtui.hpp>
#include <ftxui/component/component.hpp>
using namespace ftxui;
using namespace pvtui;
int main(int argc, char *argv[]) {
pvtui::App app(argc, argv);
auto pos_args = app.args.positional_args();
if (pos_args.size() < 2) {
printf("Usage: tutorial <prefix>\n");
return EXIT_FAILURE;
}
const std::string prefix = pos_args[1];
InputWidget desc(app, prefix + "Value.DESC", PVPutType::String);
InputWidget val(app, prefix + "Value.VAL", PVPutType::Double);
InputWidget twv(app, prefix + "TweakVal.VAL", PVPutType::Double);
ButtonWidget twf(app, prefix + "TweakFwd.PROC", " + ");
ButtonWidget twr(app, prefix + "TweakRev.PROC", " - ");
// ftxui container to define interactivity of components
auto main_container = Container::Vertical({
desc.component(),
val.component(),
twf.component(),
twr.component(),
twv.component(),
});
// ftxui renderer defines the visual layout
auto main_renderer = ftxui::Renderer(main_container, [&] {
return vbox({
hbox({
text("DESC: "),
desc.component()->Render()
| size(WIDTH, EQUAL, 15)
| EPICSColor::edit(desc),
}),
separator(),
hbox({
text("VAL: "),
val.component()->Render()
| size(WIDTH, EQUAL, 11)
| EPICSColor::edit(val),
}),
separatorEmpty(),
hbox({
twr.component()->Render(),
separatorEmpty(),
twv.component()->Render()
| size(WIDTH, EQUAL, 5)
| EPICSColor::edit(twv),
separatorEmpty(),
twf.component()->Render(),
})
}) | size(WIDTH, EQUAL, 20);
});
// Run the main loop
app.run(main_renderer);
return EXIT_SUCCESS;
}