| .vscode | ||
| .zed | ||
| config | ||
| src | ||
| tests | ||
| .clangd | ||
| .gitignore | ||
| CMakeLists.txt | ||
| CMakePresets.json | ||
| README.md | ||
cpp_template
Template for a C++23 application built from named modules, with one library and one GoogleTest binary per module.
Requirements
| Component | Version | Debian package |
|---|---|---|
| Clang | 22 | clang-22 |
| CMake | 4.3 or newer | cmake |
| Ninja | any | ninja-build |
| spdlog | any | libspdlog-dev |
| FTXUI | any | libftxui-dev |
| GoogleTest | any | libgtest-dev |
| Xlib | any | libx11-dev |
CMake 4.3 is required because the CMAKE_EXPERIMENTAL_CXX_IMPORT_STD UUID in
CMakeLists.txt is bound to that version. Ninja is required because module
dependency scanning does not work with the Makefile generators.
LVGL 9.5 and the lv:: C++ wrapper are not packaged for Debian and are fetched
from git at configure time, so the first configure needs network access and
git.
Layout
CMakeLists.txt dependencies, warning flags, subdirectories
src/CMakeLists.txt the app executable
src/modules/CMakeLists.txt one static library per module
src/modules/*.cppm module interface units
tests/CMakeLists.txt one test executable per module
tests/*_test.cpp the tests
config/lv_conf.h LVGL configuration
Modules and their dependencies:
| Module | Library depends on | Content |
|---|---|---|
math |
nothing | add, multiply |
applog |
spdlog | logger setup, level filtered log functions |
cpuload |
nothing | CPU utilization sampled from /proc/stat, load thresholds |
tui |
math, cpuload, FTXUI, pthread |
live CPU dashboard |
gui |
applog, math, cpuload, lv::lv, X11 |
the same dashboard as an LVGL window |
Build
cmake --preset default
cmake --build --preset default
./build/default/app
The application prints a few log lines, then opens the live CPU dashboard of
the tui module: a gauge for the current utilization and a chart of the last
240 samples, taken every 250 ms from /proc/stat. q or Esc leaves it and
lets the gui module follow with the same content in an LVGL window: the math
panel, a bar for the current load and a chart of the same history, at the same
sampling rate and with the same color thresholds. Closing the window ends the
application.
The window needs LV_X11_DIRECT_EXIT 0 in config/lv_conf.h. With the LVGL
default of 1 the X11 driver calls exit(0) itself when the last window closes,
and gui_demo() never returns.
Without an interactive terminal, for example when the output is piped, the
dashboard prints a single frame instead of entering the event loop, so
./build/default/app | cat terminates on its own.
The application writes app.log into the working directory it is started from.
The LVGL window is skipped when DISPLAY is unset.
Run the tests
The test binaries are built together with the application, so a plain build is enough to prepare them.
Run everything through CTest:
ctest --preset default
The preset already sets --output-on-failure. Without presets the same run is:
ctest --test-dir build/default --output-on-failure
Every TEST is registered as its own CTest case by gtest_discover_tests, so
CTest reports 35 cases rather than 5 binaries. Useful variants:
ctest --test-dir build/default -N # list the cases, run none
ctest --test-dir build/default -R MathAdd # regex over the case names
ctest --test-dir build/default -j 8 # run in parallel
ctest --test-dir build/default --rerun-failed # only what failed last time
The regex of -R matches substrings, so -R Math also selects
TuiDemo.PrintsTheValuesComputedByMath.
A single module can also be tested by calling its binary directly, which gives access to the GoogleTest options:
./build/default/tests/math_test
./build/default/tests/applog_test --gtest_filter='AppLogTest.Writes*'
./build/default/tests/tui_test --gtest_list_tests
./build/default/tests/gui_test --gtest_repeat=10 --gtest_shuffle
The five binaries are math_test, cpuload_test, applog_test, tui_test
and gui_test.
applog_test creates and deletes app.log in its working directory.
gui_test covers the label formatting and the DISPLAY guard of gui_demo,
not the LVGL event loop, which cannot run unattended. The same split applies to tui_test: it renders
frames at a fixed size through tui::render_dashboard and checks the
non-interactive path of tui_demo, while the event loop itself is not covered.
Tests can be excluded from the build entirely:
cmake --preset default -DBUILD_TESTING=OFF
Adding a module
-
Write
src/modules/<name>.cppmwithexport module <name>;. -
Add the library to
src/modules/CMakeLists.txt:add_library(<name> STATIC) target_sources(<name> PUBLIC FILE_SET CXX_MODULES FILES <name>.cppm ) target_link_libraries(<name> PRIVATE project_options) set_property(TARGET <name> PROPERTY CXX_MODULE_STD ON)The file set has to be
PUBLIC, otherwise the module cannot be imported by other targets.CXX_MODULE_STDis a target property and is not inherited, so it is set on every target, including the test executables. -
Link the new library wherever the module is imported: add
<name>to thetarget_link_librariesofappwhenmain.cppimports it, or to the module library that imports it. -
Write
tests/<name>_test.cppand register it intests/CMakeLists.txt:add_module_test(<name>_test <name>)The helper links the module library,
GTest::gtest_mainand the shared warning flags, and registers the cases with CTest.
Editor integration
.vscode/ and .zed/ contain configure, build and run tasks plus a clangd
setup with --experimental-modules-support. clangd reads
build/default/compile_commands.json, so the project has to be configured once
before diagnostics are correct.
Notes on modules
import std; is still experimental in CMake and prints a warning at generate
time. Header libraries without module support, currently LVGL and lv::, are
included in the global module fragment of the module that wraps them;
CMAKE_CXX_SCAN_FOR_MODULES is switched off around their FetchContent call
because scanning them only costs build time.