A simple starter template for building embedded Linux GUI applications with LVGL and CMake. It is intended to be used as a clean development baseline: desktop builds use SDL for fast UI preview, while device builds use the Linux/LVGL platform drivers.
In this example project, we used small MVVM-style structure around LVGL:
- UI layer: screens and widgets are implemented with LVGL objects.
BaseScreenowns the page root, title bar, nav bar, and page content. Widgets derive fromBaseWidgetsand expose abuild()entry point. - Data model:
BaseModelstores application state such as current page and theme mode. - View model:
BaseViewModelexposes model state as LVGL observer subjects and provides UI actions such as page switching, counter updates, theme toggle, and quit request. - Reactive/data binding:
src/reactivewraps LVGL observer subjects and common bindings so labels, styles, flags, widget values, and events can be connected to application state. - Data flow: user input triggers widget callbacks, callbacks update
BaseViewModel, view model publishes subjects, and bound UI objects refresh automatically through LVGL observers. - Platform layer: platform code owns Linux input integration and other hardware-facing services. The nav bar maps hardware/keyboard keys
4to8, plusESCfor quit.
Current demo UI:
- Page 1: Hello World, font weight toggle, LVGL version info, light/dark theme toggle, page navigation.
- Page 2: Counter page, increment/decrement actions with a non-negative lower bound, light/dark theme toggle, page navigation.
.
├── assets/ # Runtime assets used by the app
│ ├── applications/ # Desktop/app launcher metadata
│ ├── audio/ # Audio resources
│ ├── fonts/ # TTF fonts loaded through FreeType
│ └── images/ # Image resources
├── screenshot/ # Simulator screenshots for supported desktop platforms
├── src/
│ ├── app/ # Application lifecycle, asset loading, screen management
│ ├── config/ # LVGL config headers for desktop and device builds
│ ├── logger/ # Project logging wrapper
│ ├── model/ # Base application data model
│ ├── platform/ # Platform input/hardware integration
│ ├── reactive/ # LVGL observer subjects and binding helpers
│ ├── view/ # Theme, UI constants, screens, and widgets
│ │ ├── screens/ # Page-level LVGL screens
│ │ └── widgets/ # Reusable LVGL widgets such as nav bar and icon button
│ ├── viewmodel/ # BaseViewModel and UI-facing actions/state subjects
│ └── main.cpp # Program entry point
├── CMakeLists.txt # Build graph, dependencies, options, targets
├── CMakePresets.json # Desktop/device configure and build presets
└── README.md
src/app/asset_manager.*: resolves runtime assets from the optional CMake asset root, source treeassets/, and installed/usr/share/<APP_NAME>/path.src/app/screen_namager.cpp: switches LVGL screens when the current page subject changes.src/config/lv_conf_desktop.h: desktop SDL simulator LVGL settings.src/config/lv_conf_cm0.h: embedded Linux/CardputerZero LVGL settings.src/platform/linux_input.*: Linux evdev keypad support and desktop SDL keyboard routing for nav buttons.src/reactive/subjects.*: typed wrappers around LVGL subjects.src/reactive/bindings.*: helper bindings for labels, theme, widget values, flags, states, and events.src/view/widgets/navbar.*: bottom navigation bar and icon-button action mapping.src/view/widgets/icon_button.*: transparent icon button with font/color/theme support.
Common CMake cache options:
| Option | Default | Description |
|---|---|---|
USE_DESKTOP |
ON |
Build SDL desktop simulator when ON; build embedded Linux target when OFF. |
APP_NAME |
template_app |
Application name used by installed asset lookup. |
APP_ASSETS_ROOT |
empty | Optional runtime asset root. Expected layout includes fonts/, images/, etc. |
APP_KEY_INPUT_DEVICE |
empty | Optional Linux evdev device path, e.g. /dev/input/event0. Empty means auto-scan /dev/input/event*. |
APP_FRAMEBUFFER_DEVICE |
/dev/fb0 |
Linux framebuffer device used by embedded builds when APP_USE_DRM=OFF. |
APP_USE_DRM |
OFF |
Use LVGL's Linux DRM/KMS backend instead of fbdev for embedded builds. |
APP_DRM_DEVICE |
/dev/dri/card0 |
DRM device path used when APP_USE_DRM=ON. |
APP_DRM_CONNECTOR_ID |
-1 |
DRM connector id used when APP_USE_DRM=ON; -1 auto-selects. |
Asset lookup order:
APP_ASSETS_ROOTwhen provided by CMake- source-tree
assets/for development /usr/share/<APP_NAME>/for installed deployments
Desktop builds are intended for fast UI development and use SDL as the LVGL display/input backend.
| macOS simulator | Debian simulator | Windows simulator |
|---|---|---|
![]() |
![]() |
![]() |
Current dependencies info:
| Dependency | Version | Source | Notes |
|---|---|---|---|
| LVGL | v9.5.0 |
CMakeLists.txt FetchContent |
Main GUI framework. |
| fmt | 12.1.0 |
System package manager or vcpkg.json override |
Logging and formatted strings. |
| libpng | 1.6.48 |
System package manager or vcpkg.json override |
PNG image decoding support for LVGL. |
| libjpeg-turbo | 3.1.3 |
System package manager or vcpkg.json override |
JPEG image decoding support for LVGL. |
| zlib | 1.3.1 |
System package manager or vcpkg.json override |
Compression dependency used by image libraries. |
| SDL2 | 2.32.54 |
System package manager or vcpkg.json override |
Desktop simulator display and input backend. |
| FreeType | 2.13.3 |
System package manager or vcpkg.json override |
Runtime TTF font rendering. |
Windows vcpkg installs use the versions above through manifest mode, with the registry baseline pinned in
vcpkg-configuration.json.
Debian/Ubuntu dependencies:
sudo apt update
sudo apt install -y \
build-essential \
cmake \
ninja-build \
libpng-dev \
libjpeg-dev \
libfmt-dev \
libsdl2-dev \
libfreetype-dev \
zlib1g-devNote
Minimal CMake version is 3.31.0 (in coordinate with Debian 13 trixie), for Ubuntu user you can install latest cmake via snap
and run following cmake command with /snap/bin/cmake.
Configure:
cmake --preset linux-x86-64Build:
cmake --build --preset linux-x86-64-dbg
# alternatively, you can run release build
# cmake --build --preset linux-x86-64-relRun:
./build/linux-x86-64/Debug/template_app
# or launch release build
# ./build/linux-x86-64/Release/template_appInstall the Apple command-line tools:
xcode-select --installInstall dependencies with Homebrew:
brew install \
cmake \
ninja \
libpng \
jpeg \
fmt \
sdl2 \
freetype \
dpkg \
zlibConfigure for Apple Silicon:
cmake --preset darwin-arm64Build:
cmake --build --preset darwin-arm64-dbg
# alternatively, you can run release build
# cmake --build --preset darwin-arm64-relRun:
./build/darwin-arm64/Debug/template_app
# or launch release build
# ./build/darwin-arm64/Release/template_appFor Intel macOS, use the darwin-x86-64 configure preset and matching build preset:
cmake --preset darwin-x86-64
cmake --build --preset darwin-x86-64-dbg
# alternatively, you can run release build
# cmake --build --preset darwin-x86-64-rel
./build/darwin-x86-64/Debug/template_app
# or launch release build
# ./build/darwin-x86-64/Release/template_appInstall CMake and Ninja:
winget install Kitware.CMake
winget install Ninja-build.NinjaInstall a C++ toolchain. Choose one of the following.
MSVC Build Tools:
winget install Microsoft.VisualStudio.BuildToolsIn the Visual Studio installer, enable Desktop development with C++ and make sure MSVC and a Windows SDK are selected.
MinGW-w64:
winget install BrechtSanders.WinLibs.POSIX.UCRTAlternatively, you can download from winlibs.
Install vcpkg:
git clone https://github.com/microsoft/vcpkg.git C:\vcpkg
C:\vcpkg\bootstrap-vcpkg.bat -disableMetricsConfigure environmental variables for current terminal:
$env:VCPKG_ROOT="C:\vcpkg"
$env:PATH="$env:VCPKG_ROOT;$env:PATH"
Tip
Change VCPKG_ROOT to your vcpkg installation directory if it is located elsewhere.
These environment variables are session-scoped, meaning they only apply to the current terminal session. Once the terminal is closed, the variables will be lost and need to be set again in a new session. If you want to a persist environment variable
setx VCPKG_ROOT "C:\vcpkg"Configure and build with MSVC:
Important
These steps require launching CMake from a Developer Command Prompt or Developer PowerShell provided by Visual Studio.
This ensures that the MSVC environment (compiler, linker, and Windows SDK paths) is properly initialized.
Without this environment setup, CMake may fail to detect or correctly configure the MSVC toolchain.
cmake --preset win32-msvc
cmake --build --preset win32-msvc-dbg
.\build\msvc\Debug\template_app.exe
# alternatively for release build
# cmake --build --preset win32-msvc-rel
# .\build\msvc\Release\template_app.exeConfigure and build with MinGW-w64:
Important
These steps require a properly configured MinGW-w64 toolchain in your system PATH.
Please ensure the following compilers are available in the current terminal session:
- gcc
- g++
- ld
- ar
You can verify the setup by running:
gcc --version
g++ --versionIf these commands are not recognized, you must add MinGW-w64 bin directory to your PATH before configuring CMake.
cmake --preset win32-mingw64
cmake --build --preset win32-mingw64-dbg
.\build\mingw64\Debug\template_app.exe
# alternatively for release build
# cmake --build --preset win32-mingw64-rel
#.\build\mingw64\Release\template_app.exeNote
VCPKG will handle the dependencies during CMake configuration process automatically,
this may take several minutes depend on you network connection.
This preset builds an aarch64 Linux target from a host machine with an aarch64-linux-gnu-gcc/g++ toolchain.
The BSP at .cache/sdk_bsp-src is treated as a sysroot: headers, libraries, startup objects, and pkg-config metadata are resolved from that directory first.
If that directory does not exist on the first configure, the toolchain downloads and extracts sdk_bsp.tar.gz automatically before CMake runs compiler checks.
Install a cross compile toolchain:
- Debian
sudo apt install crossbuild-essential-arm64- MacOS
brew install aarch64-unknown-linux-gnuNote
On Windows, it's recommended to use WSL for cross build
or you can download cross compile tools and configure the PATH yourself
Configure:
cmake --preset cp0-crossBuild:
cmake --build --preset cp0-cross-dbg
# or release build
# cmake --build --preset cp0-cross-relIf your BSP sysroot is in another location, pass it explicitly:
cmake --preset cp0-cross -DCM0_SDK_ROOT=/path/to/sdk_bsp-srcTo force an offline configure and fail when the BSP is missing, use:
cmake --preset cp0-cross -DCM0_ALLOW_FETCH_DEPS=OFFThe bottom nav bar maps hardware/keyboard keys from left to right:
| Key | Nav item |
|---|---|
4 |
First icon button |
5 |
Second icon button |
6 |
Third icon button |
7 |
Fourth icon button |
8 |
Fifth icon button |
ESC |
First icon button / quit |
- Add a new page enum value in
src/model/base_model.h. - Add the page transition/state logic in
BaseModelandBaseViewModel. - Create a screen under
src/view/screens/deriving fromBaseScreen. - Extend
ScreenManagerto load the new screen when the page subject changes. - Update
NavBaricons and callbacks as needed.
- Derive from
BaseWidgets. - Implement
build()and create LVGL objects underparent_. - Use
reactive::bind_*helpers for text, style, state, or event bindings. - Keep LVGL observer lifetimes object-bound whenever possible with
lv_subject_add_observer_objorreactive::observe_obj.
Fonts are loaded at runtime through FreeType. Place development fonts in:
assets/fonts/
Installed deployments can place fonts in:
/usr/share/<APP_NAME>/fonts/
You can also provide an asset root at configure time:
cmake --preset darwin-arm64 -DAPP_ASSETS_ROOT=/path/to/assetsDebian packages are produced with CPack and written to dist/. The package file name follows:
<AppName>_<Version>_m5stack1_arm64.deb
Default example:
dist/TemplateApp_0.0.1_m5stack1_arm64.deb
Package layout:
| Path | Content |
|---|---|
/usr/bin/template_app |
Application executable. |
/usr/share/template_app/ |
Runtime assets: fonts, images, audio. |
/usr/share/APPLaunch/applications/template_app.desktop |
APPLaunch launcher entry. |
/usr/share/APPLaunch/share/images/template*.png |
APPLaunch launcher icons/fallbacks. |
/usr/lib/systemd/system/template_app.service |
Optional systemd service for embedded autostart. |
/usr/share/doc/template_app/ |
README and third-party asset license notes. |
Build and package:
cmake --preset cp0-cross
cmake --build --preset cp0-cross-rel
cpack --preset cp0-cross-debOr run the full configure, release build, and package flow with one workflow preset:
cmake --workflow --preset cp0-cross-packageNote
Use Debian based Linux host with CPack generator as other OS may not resolve the dependencies correctly no full dpkg, dpkg-shlibdeps, objdump and readelf support
If you have better framework designs on Embedded Linux or build system practice, feel free to let us know.
Project released under MIT license. More license information can be found in assets folder.


