For the purposes of this tutorial, we will create the application on Linux.
To bridge the connection between Qt's C++ API and our Rust code, we will be using cxx-qt, which provides two way communication between the two.
You will need to install Rust, Cargo, CMake, extra-cmake-modules, QtQuick and Kirigami. All other software needed to build the project will be provided via Rust crates from crates.io or directly from a git repository.
Upstream Rust recommends using rustup to install Rust and Cargo:
curl https://sh.rustup.rs -sSf | sh
You may otherwise install them from your distribution repositories.
The application will be a simple Markdown viewer called simplemdviewer and its executable will have the same name.
By the end of the tutorial, the project will look like this:
simplemdviewer/
├── README.md
├── CMakeLists.txt # To manage installing the project
├── Cargo.toml # To manage building the project
├── build.rs # To initialize cxx-qt
├── org.kde.simplemdviewer.desktop
├── org.kde.simplemdviewer.json
├── org.kde.simplemdviewer.svg
├── org.kde.simplemdviewer.metainfo.xml
└── src/
├── main.rs # The entrypoint to our application
├── mdconverter.rs # The Markdown formatter
└── qml/
└── Main.qml # Where the Kirigami window will be made
💡 Tip
To quickly generate this folder structure, run:
cargo new simplemdviewer
mkdir -p simplemdviewer/src/qml/
Setting up the project
The UI will be created in QML and the logic in Rust. Users will write some
Markdown text, press a button, and the formatted text will be shown below it.
Initially we will focus on the following files, do a test run, and then add the rest later:
#[cxx_qt::bridge]modffi{extern"RustQt"{#[qobject]typeDummyQObject=super::DummyRustStruct;}}#[derive(Default)]pubstructDummyRustStruct;usecxx_qt_lib::{QGuiApplication,QQmlApplicationEngine,QQuickStyle,QString,QUrl};usecxx_qt_lib_extras::QApplication;usestd::env;fnmain(){letmutapp=QApplication::new();// To associate the executable to the installed desktop file
QGuiApplication::set_desktop_file_name(&QString::from("org.kde.simplemdviewer"));// To ensure the style is set correctly
ifenv::var("QT_QUICK_CONTROLS_STYLE").is_err(){QQuickStyle::set_style(&QString::from("org.kde.desktop"));}letmutengine=QQmlApplicationEngine::new();ifletSome(engine)=engine.as_mut(){engine.load(&QUrl::from("qrc:/qt/qml/org/kde/simplemdviewer/src/qml/Main.qml",));}ifletSome(app)=app.as_mut(){app.exec();}}
The first part that is marked with the #[cxx_qt::bridge] Rust macro creates a dummy QObject out of a dummy Rust struct. This is needed as cxx-qt needs to find at least one QObject exposed to Rust to work. This should no longer be necessary in the future once https://github.com/KDAB/cxx-qt/issues/1137 is addressed. When we start Adding Markdown functionality later on, we will use a proper QObject.
We then created a
QGuiApplication
object that initializes the application and contains the main event loop. The
QQmlApplicationEngine
object loads the Main.qml file.
Then comes the part that actually creates the application window:
The long URL qrc:/qt/qml/org/kde/simplemdviewer/src/qml/Main.qml corresponds to the Main.qml file in the Qt Resource System, and it follows this scheme: <resource_prefix><import_URI><QML_dir><file>.
In other words: the default resource prefix qrc:/qt/qml/ + the import URI org/kde/simplemdviewer (set in build.rs, separated by slashes instead of dots) + the QML dir src/qml/ + the QML file Main.qml.
src/qml/Main.qml
Create a new src/qml/ directory and add a Main.qml to it:
importQtQuickimportQtQuick.LayoutsimportQtQuick.ControlsasControlsimportorg.kde.kirigamiasKirigamiKirigami.ApplicationWindow{id: roottitle:"Simple Markdown Viewer in Rust 🦀"minimumWidth:Kirigami.Units.gridUnit*20minimumHeight:Kirigami.Units.gridUnit*20width:minimumWidthheight:minimumHeightpageStack.initialPage:initPageComponent{id: initPageKirigami.Page{title:"Markdown Viewer"ColumnLayout{anchors{top:parent.topleft:parent.leftright:parent.right}Controls.TextArea{id: sourceAreaplaceholderText:"Write some Markdown code here"wrapMode:Text.WrapAnywhereLayout.fillWidth:trueLayout.minimumHeight:Kirigami.Units.gridUnit*5}RowLayout{Layout.fillWidth:trueControls.Button{text:"Format"onClicked:formattedText.text=sourceArea.text}Controls.Button{text:"Clear"onClicked:{sourceArea.text=""formattedText.text=""}}}Controls.Label{id: formattedTexttextFormat:Text.RichTextwrapMode:Text.WordWraptext:sourceArea.textLayout.fillWidth:trueLayout.minimumHeight:Kirigami.Units.gridUnit*5}}}}}
This is the file that will manage how the window will look like. For more details about Kirigami, see our Kirigami Tutorial.
Cargo.toml
Create a new Cargo.toml file in the root directory of your project:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[package]name="simplemdviewer"version="0.1.0"authors=["Konqi the Konqueror <konqi@kde.org>"]edition="2021"license="BSD-2-Clause"[dependencies]cxx="1.0.122"cxx-qt="0.7"cxx-qt-lib={version="0.7",features=["qt_full"]}cxx-qt-lib-extras="0.7"markdown="=1.0.0-alpha.17"[build-dependencies]# The link_qt_object_files feature is required for statically linking Qt 6.cxx-qt-build={version="0.7",features=["link_qt_object_files"]}
This file is what is traditionally used for building Rust applications, and indeed with only the four files we've written it's already possible to build the project with Cargo:
cargo build
cargo run
The project will be built inside a directory called target/, and when running cargo run Cargo will execute the resulting binary in that directory.
You may also specify a custom build directory with the --target-dir flag. This will be important in the CMakeLists.txt file.
cargo build --target-dir build/
cargo run --target-dir build/
CMakeLists.txt
Cargo lacks the ability to install more than just executables, which is insufficient for desktop applications. We need to install:
an application icon
a Desktop Entry file to have a menu entry and show the application icon in our window on Wayland
a Metainfo file to show the application on Linux software stores
More complex and mature projects might also include manpages and manuals, KConfig configuration files, and D-Bus interfaces and services for example.
To achieve this, we can wrap Cargo with CMake and have it manage the installation step for us. For now we will focus only on the executable, and add the other files in the section Adding metadata.
Create a new CMakeLists.txt file in the root directory of the project:
The first thing we do is add KDE's Extra CMake Modules (ECM) to our project so we can use ecm_find_qml_module to check that Kirigami is installed when trying to build the application, and if it's not, fail immediately. Another useful ECM feature is ECMUninstallTarget, which allows to easily uninstall the application with CMake if desired.
We create a target that will simply execute Cargo when run, and mark it with ALL so it builds by default.
After this, we simply install the simplemdviewer executable generated by Cargo in the binary directory and install it to the BINDIR, which is usually /usr/bin, /usr/local/bin or ~/.local/bin.
Note that the name simplemdviewer set in lines 14 and 20 must match the application's executable name built by Cargo, otherwise the project will fail to install.
In line 16 we use --target-dir to make Cargo build the executable inside CMake's binary directory (typically build/). This way, if the user specifies an out-of-tree build directory in their CMake build command, Cargo will use the directory specified by the user instead of the target/ directory:
Some distributions might name the qml binary from Qt5 or Qt6 differently: for example, openSUSE and Arch have qml, qt5-qml, and qml6, while Fedora has qml, qml-qt5, and qml-qt6.
Use the explicit Qt6 variant to be sure that your app runs with Qt6.
It does not format anything; if we click on "Format" it just spits the
unformatted text into a text element. It also lacks a window icon.
Adding Markdown functionality
Let’s add some Rust logic: a simple Markdown converter in a
Rust QObject
derivative struct.
The highlighted lines show the same essential contents that we added to our initial dummy QObject in main.rs, but this time using an actual QObject the cxx-qt way. Let's take a look bit by bit.
We have a struct that has the Default trait, so all its members are default initialized:
We then create a module inside the new mdconverter module that we call ffi (which stands for Foreign Function Interface) because it will deal with things coming from C++ or C++ code that is generated from our Rust code. The name does not need to be ffi. The module gets marked with the #[cxx-qt::bridge] macro first, and we import QString which comes from the Qt C++ side so it's available on the Rust side:
The #[auto_cxx_name] cxx-qt macro is a convenience macro that replaces names from the Rust snake_case convention (md_format()) to the C++ Qt camelCase convention (mdFormat()).
We then use extern "RustQt" so we can define our new QObject. The Rust code in it generates C++ code that lets us expose it to QML. In this case, the type MdConverter will be exposed.
We use the #[qml_element] attribute to mark the new QObject as something that contains code exposed to QML, analogous to C++ QML_ELEMENT.
The #[qproperty()] attribute specifies the exact types that will be exposed to QML, analogous to the C++ Q_PROPERTY. In this case, we want to export source_text as the MdConverter.sourceText QML property, which will be used in the Rust implementation.
We use #[qinvokable] to expose the function md_format() to QML.
Lastly, we implement the function that actually formats text as Markdown:
We use the markdown crate for this. This function returns a QString made of the MdConverter.sourceText property converted from Markdown to HTML. Non-editable Qt text components parse HTML in strings by default, so the QString will be shown formatted in our application without further changes.
Now the mdconverter module exposes everything we need to the QML side:
the MdConverter QML type
the sourceText QML property
the mdFormat() QML method
src/main.rs
Two modifications are needed in src/main.rs: removing the dummy QObject and importing our new mdconverter module:
// #[cxx_qt::bridge]
// mod ffi {
// extern "RustQt" {
// #[qobject]
// type DummyQObject = super::DummyRustStruct;
// }
// }
//
// #[derive(Default)]
// pub struct DummyRustStruct;
usecxx_qt_lib::{QGuiApplication,QQmlApplicationEngine,QQuickStyle,QString,QUrl};usecxx_qt_lib_extras::QApplication;usestd::env;modmdconverter;fnmain(){letmutapp=QApplication::new();// To associate the executable to the installed desktop file
QGuiApplication::set_desktop_file_name(&QString::from("org.kde.simplemdviewer"));// To ensure the style is set correctly
ifenv::var("QT_QUICK_CONTROLS_STYLE").is_err(){QQuickStyle::set_style(&QString::from("org.kde.desktop"));}letmutengine=QQmlApplicationEngine::new();ifletSome(engine)=engine.as_mut(){engine.load(&QUrl::from("qrc:/qt/qml/org/kde/simplemdviewer/src/qml/Main.qml"));}ifletSome(app)=app.as_mut(){app.exec();}}
build.rs
Now that the module is finished, replace src/main.rs with the new src/mdconverter.rs file in build.rs:
Since we are going to actually use code that was exposed to QML, we need to actually import it this time. The module from the file mdconverter.rs is made available via the QML module from build.rs under the org.kde.simplemdviewer import URI.
importQtQuickimportQtQuick.ControlsasControlsimportQtQuick.Layoutsimportorg.kde.kirigamiasKirigamiimportorg.kde.simplemdviewerKirigami.ApplicationWindow{id: roottitle:"Simple Markdown Viewer in Rust 🦀"minimumWidth:Kirigami.Units.gridUnit*20minimumHeight:Kirigami.Units.gridUnit*20width:minimumWidthheight:minimumHeightpageStack.initialPage:initPageComponent{id: initPageKirigami.Page{title:"Markdown Viewer"MdConverter{id: mdconvertersourceText:sourceArea.text}ColumnLayout{anchors{top:parent.topleft:parent.leftright:parent.right}Controls.TextArea{id: sourceAreaplaceholderText:"Write some Markdown code here"wrapMode:Text.WrapAnywhereLayout.fillWidth:trueLayout.minimumHeight:Kirigami.Units.gridUnit*5}RowLayout{Layout.fillWidth:trueControls.Button{text:"Format"onClicked:formattedText.text=mdconverter.mdFormat()}Controls.Button{text:"Clear"onClicked:{sourceArea.text="";formattedText.text="";}}}Controls.Label{id: formattedTexttextFormat:Text.RichTextwrapMode:Text.WordWrapLayout.fillWidth:trueLayout.minimumHeight:Kirigami.Units.gridUnit*5}}}}}
The contents of the sourceText property is populated with the contents of the sourceArea.text, which is then formatted when clicking the "Format" button and passed to the formattedText.
Adding metadata
The finishing touches need to be done for the application to have a nice window icon and look good in software stores.
README.md
Create a simple README.md:
1
2
3
# Simple Markdown Viewer in Rust and Kirigami 🦀
A simple Markdown viewer created with Kirigami, QML and Rust.
org.kde.simplemdviewer.svg
For this tutorial the well known Markdown icon is okay.
Get the
Markdown
icon and save it as simplemdviewer/org.kde.simplemdviewer.svg:
We need the icon to be perfectly squared, which can be accomplished with
Inkscape:
Open org.kde.simplemdviewer.svg in Inkscape.
Type Ctrl+a to select everything.
On the top W: text field, type 128 and press Enter.
Go to File -> Document Properties...
Change the Width: text field to 128 and press Enter.
Save the file.
org.kde.simplemdviewer.desktop
The primary purpose of Desktop Entry files is to show your app on the application launcher on Linux. Another reason to have them is to have window icons on Wayland, as they are required to tell the compositor "this window goes with this icon".
It must follow a reverse-DNS naming scheme followed by the .desktop extension such as org.kde.simplemdviewer.desktop:
1
2
3
4
5
6
7
[Desktop Entry]Name=Simple Markdown Viewer in Rust and KirigamiExec=simplemdviewerIcon=org.kde.simplemdviewerType=ApplicationTerminal=falseCategories=Utility
Note that the icon name should not include its file extension.
org.kde.simplemdviewer.metainfo.xml
To show your application on software stores like Plasma Discover, GNOME Software or Flathub, your application will need to provide an AppStream metainfo file:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8"?><componenttype="desktop-application"><id>org.kde.simplemdviewer</id><launchabletype="desktop-id">org.kde.simplemdviewer.desktop</launchable><name>Simple Markdown Viewer in Rust and Kirigami</name><metadata_license/><project_license>MPL-2.0</project_license><summary>A simple Markdown viewer application</summary><developer_name>Konqi the Konqueror</developer_name><update_contact>konqi@kde.org</update_contact><releases><releaseversion="0.1.0"date="2025-07-01"/></releases></component>
CMakeLists.txt
Now that we have our final metadata files, we can tell CMake where to install them:
When a distribution installs these files, they will go to /usr/share/applications, /usr/share/metainfo and /usr/share/icons/hicolor/scalable/apps, respectively. We will be installing them in userspace, so ~/.local/share/applications, ~/.local/share/metainfo and ~/.local/share/icons/hicolor/scalable/apps instead.
[package]name="simplemdviewer"version="0.1.0"authors=["Konqi the Konqueror <konqi@kde.org>"]edition="2021"license="BSD-2-Clause"[dependencies]cxx="1.0"cxx-qt="0.7"cxx-qt-lib={version="0.7",features=["qt_full"]}cxx-qt-lib-extras="0.7"markdown="=1.0.0-alpha.17"[build-dependencies]# The link_qt_object_files feature is required for statically linking Qt 6.cxx-qt-build={version="0.7",features=["link_qt_object_files"]}
// #[cxx_qt::bridge]
// mod ffi {
// extern "RustQt" {
// #[qobject]
// type DummyQObject = super::DummyRustStruct;
// }
// }
//
// #[derive(Default)]
// pub struct DummyRustStruct;
usecxx_qt_lib::{QGuiApplication,QQmlApplicationEngine,QQuickStyle,QString,QUrl};usecxx_qt_lib_extras::QApplication;usestd::env;modmdconverter;fnmain(){letmutapp=QApplication::new();// To associate the executable to the installed desktop file
QGuiApplication::set_desktop_file_name(&QString::from("org.kde.simplemdviewer"));// To ensure the style is set correctly
ifenv::var("QT_QUICK_CONTROLS_STYLE").is_err(){QQuickStyle::set_style(&QString::from("org.kde.desktop"));}letmutengine=QQmlApplicationEngine::new();ifletSome(engine)=engine.as_mut(){engine.load(&QUrl::from("qrc:/qt/qml/org/kde/simplemdviewer/src/qml/Main.qml"));}ifletSome(app)=app.as_mut(){app.exec();}}
importQtQuickimportQtQuick.ControlsasControlsimportQtQuick.Layoutsimportorg.kde.kirigamiasKirigamiimportorg.kde.simplemdviewerKirigami.ApplicationWindow{id: roottitle:"Simple Markdown Viewer in Rust 🦀"minimumWidth:Kirigami.Units.gridUnit*20minimumHeight:Kirigami.Units.gridUnit*20width:minimumWidthheight:minimumHeightpageStack.initialPage:initPageComponent{id: initPageKirigami.Page{title:"Markdown Viewer"MdConverter{id: mdconvertersourceText:sourceArea.text}ColumnLayout{anchors{top:parent.topleft:parent.leftright:parent.right}Controls.TextArea{id: sourceAreaplaceholderText:"Write some Markdown code here"wrapMode:Text.WrapAnywhereLayout.fillWidth:trueLayout.minimumHeight:Kirigami.Units.gridUnit*5}RowLayout{Layout.fillWidth:trueControls.Button{text:"Format"onClicked:formattedText.text=mdconverter.mdFormat()}Controls.Button{text:"Clear"onClicked:{sourceArea.text="";formattedText.text="";}}}Controls.Label{id: formattedTexttextFormat:Text.RichTextwrapMode:Text.WordWrapLayout.fillWidth:trueLayout.minimumHeight:Kirigami.Units.gridUnit*5}}}}}
org.kde.simplemdviewer.desktop
1
2
3
4
5
6
7
[Desktop Entry]Name=Simple Markdown Viewer in Rust and KirigamiExec=simplemdviewerIcon=org.kde.simplemdviewerType=ApplicationTerminal=falseCategories=Utility
org.kde.simplemdviewer.metainfo.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8"?><componenttype="desktop-application"><id>org.kde.simplemdviewer</id><launchabletype="desktop-id">org.kde.simplemdviewer.desktop</launchable><name>Simple Markdown Viewer in Rust and Kirigami</name><metadata_license/><project_license>MPL-2.0</project_license><summary>A simple Markdown viewer application</summary><developer_name>Konqi the Konqueror</developer_name><update_contact>konqi@kde.org</update_contact><releases><releaseversion="0.1.0"date="2025-07-01"/></releases></component>