Skip to main content
Skip to content

A full Rust + Kirigami application

Learn how to write an application with cxx-qt

Prerequisites

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.

logo of Linux operating system ManjaroManjarologo of Linux operating system Arch LinuxArch
sudo pacman -S rust cargo cmake extra-cmake-modules kirigami flatpak-builder qqc2-desktop-style appstream
logo of Linux operating system openSUSEOpenSUSE
sudo zypper install rust cargo cmake kf6-extra-cmake-modules kf6-kirigami-devel kf6-qqc2-desktop-style-devel qt6-declarative-devel qt6-wayland-devel flatpak-builder AppStream-compose
logo of Linux operating system FedoraFedora
sudo dnf install rust cargo cmake extra-cmake-modules kf6-kirigami-devel kf6-qqc2-desktop-style qt6-qtdeclarative-devel qt6-qtwayland-devel flatpak-builder appstream-compose

Project Structure

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

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:

simplemdviewer/
├── CMakeLists.txt
├── Cargo.toml
├── build.rs
└── src/
    ├── main.rs
    └── qml/
        └── Main.qml

build.rs

We need to initialize cxx-qt before Cargo builds the project, hence we use a build script build.rs which serves this purpose.

Create a new build.rs file in the root directory of the project with the following contents:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
use cxx_qt_build::{CxxQtBuilder, QmlModule};

fn main() {
    CxxQtBuilder::new()
        .qml_module(QmlModule {
            uri: "org.kde.simplemdviewer",
            qml_files: &["src/qml/Main.qml"],
            rust_files: &["src/main.rs"],
            ..Default::default()
        })
        .build();
}

src/main.rs

Create a new directory src/ and add a new main.rs file in this directory:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#[cxx_qt::bridge]
mod ffi {
    extern "RustQt" {
        #[qobject]
        type DummyQObject = super::DummyRustStruct;
    }
}

#[derive(Default)]
pub struct DummyRustStruct;

use cxx_qt_lib::{QGuiApplication, QQmlApplicationEngine, QQuickStyle, QString, QUrl};
use cxx_qt_lib_extras::QApplication;
use std::env;

fn main() {
    let mut app = 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
    if env::var("QT_QUICK_CONTROLS_STYLE").is_err() {
        QQuickStyle::set_style(&QString::from("org.kde.desktop"));
    }

    let mut engine = QQmlApplicationEngine::new();
    if let Some(engine) = engine.as_mut() {
        engine.load(&QUrl::from(
            "qrc:/qt/qml/org/kde/simplemdviewer/src/qml/Main.qml",
        ));
    }

    if let Some(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:

31
32
33
34
        engine.load(&QUrl::from(
            "qrc:/qt/qml/org/kde/simplemdviewer/src/qml/Main.qml"
        ));
    }

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls as Controls
import org.kde.kirigami as Kirigami

Kirigami.ApplicationWindow {
    id: root

    title: "Simple Markdown Viewer in Rust 🦀"

    minimumWidth: Kirigami.Units.gridUnit * 20
    minimumHeight: Kirigami.Units.gridUnit * 20
    width: minimumWidth
    height: minimumHeight

    pageStack.initialPage: initPage

    Component {
        id: initPage

        Kirigami.Page {
            title: "Markdown Viewer"

            ColumnLayout {
                anchors {
                    top: parent.top
                    left: parent.left
                    right: parent.right
                }
                Controls.TextArea {
                    id: sourceArea

                    placeholderText: "Write some Markdown code here"
                    wrapMode: Text.WrapAnywhere
                    Layout.fillWidth: true
                    Layout.minimumHeight: Kirigami.Units.gridUnit * 5
                }

                RowLayout {
                    Layout.fillWidth: true

                    Controls.Button {
                        text: "Format"

                        onClicked: formattedText.text = sourceArea.text
                    }

                    Controls.Button {
                        text: "Clear"

                        onClicked: {
                            sourceArea.text = ""
                            formattedText.text = ""
                        }
                    }
                }

                Controls.Label {
                    id: formattedText

                    textFormat: Text.RichText
                    wrapMode: Text.WordWrap
                    text: sourceArea.text

                    Layout.fillWidth: true
                    Layout.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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
cmake_minimum_required(VERSION 3.28)

project(simplemdviewer)

find_package(ECM 6.0 REQUIRED NO_MODULE)
set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH})
include(KDEInstallDirs)
include(ECMUninstallTarget)

include(ECMFindQmlModule)
ecm_find_qmlmodule(org.kde.kirigami REQUIRED)
find_package(KF6 REQUIRED COMPONENTS QQC2DesktopStyle)

add_custom_target(simplemdviewer
    ALL
    COMMAND cargo build --target-dir ${CMAKE_CURRENT_BINARY_DIR}
)

install(
    PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/debug/simplemdviewer
    DESTINATION ${KDE_INSTALL_BINDIR}
)

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 also use CMake's find_package() to make sure we have qqc2-desktop-style, KDE's QML style for the desktop.

 5
 6
 7
 8
 9
10
11
12
find_package(ECM 6.0 REQUIRED NO_MODULE)
set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH})
include(KDEInstallDirs)
include(ECMUninstallTarget)

include(ECMFindQmlModule)
ecm_find_qmlmodule(org.kde.kirigami REQUIRED)
find_package(KF6 REQUIRED COMPONENTS QQC2DesktopStyle)

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.

14
15
16
17
18
19
20
21
22
add_custom_target(simplemdviewer
    ALL
    COMMAND cargo build --target-dir ${CMAKE_CURRENT_BINARY_DIR}
)

install(
    PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/debug/simplemdviewer
    DESTINATION ${KDE_INSTALL_BINDIR}
)

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:

cmake -B build/
cmake --build build/

For more information about CMake, targets, and the binary directory, see Building KDE software manually.

First test run

We have just created a simple QML-Kirigami-Rust application. Run it:

cmake -B build --install-prefix ~/.local
cmake --build build/
./build/debug/simplemdviewer

You can also try running it with Cargo:

cargo build --target-dir build/ # Optional; cargo run will also build the project!
cargo run --target-dir build/

At the moment we have not used any interesting Rust stuff. In reality, the application can also run as a standalone QML app:

QT_QUICK_CONTROLS_STYLE=org.kde.desktop qml6 --apptype widget src/qml/Main.qml

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.

A screenshot of the application showing a text area with two buttons Format and Clear beneath it. The text area has the text "# Hello", yet the text below the buttons shows the same text, unformatted.

Adding Markdown functionality

Let’s add some Rust logic: a simple Markdown converter in a Rust QObject derivative struct.

src/mdconverter.rs

Create a new mdconverter.rs file inside src/:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
use std::pin::Pin;

use cxx_qt_lib::QString;

#[cxx_qt::bridge]
mod ffi {

    unsafe extern "C++" {
        include!("cxx-qt-lib/qstring.h");
        type QString = cxx_qt_lib::QString;
    }

    #[auto_cxx_name]
    unsafe extern "RustQt" {
        #[qobject]
        #[qml_element]
        #[qproperty(QString, source_text)]
        type MdConverter = super::MdConverterStruct;

        #[qinvokable]
        fn md_format(self: Pin<&mut MdConverter>) -> QString;
    }
}

#[derive(Default)]
pub struct MdConverterStruct {
    source_text: QString,
}

impl ffi::MdConverter {
    pub fn md_format(self: Pin<&mut Self>) -> QString {
        QString::from(&markdown::to_html(&self.source_text().to_string()))
    }
}

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:

25
26
27
28
#[derive(Default)]
pub struct MdConverterStruct {
    source_text: QString,
}

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:

 5
 6
 7
 8
 9
10
11
#[cxx_qt::bridge]
mod ffi {

    unsafe extern "C++" {
        include!("cxx-qt-lib/qstring.h");
        type QString = cxx_qt_lib::QString;
    }

We use the type keyword to create an alias so that we don't need to type the cxx_qt_lib::QString name all the time.

After that, we write the aforementioned Rust code that will generate a C++ Qt QObject:

13
14
15
16
17
18
19
20
21
22
    #[auto_cxx_name]
    unsafe extern "RustQt" {
        #[qobject]
        #[qml_element]
        #[qproperty(QString, source_text)]
        type MdConverter = super::MdConverterStruct;

        #[qinvokable]
        fn md_format(self: Pin<&mut MdConverter>) -> QString;
    }

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:

30
31
32
33
impl ffi::MdConverter {
    pub fn md_format(self: Pin<&mut Self>) -> QString {
        QString::from(&markdown::to_html(&self.source_text().to_string()))
    }

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// #[cxx_qt::bridge]
// mod ffi {
//     extern "RustQt" {
//         #[qobject]
//         type DummyQObject = super::DummyRustStruct;
//     }
// }
//
// #[derive(Default)]
// pub struct DummyRustStruct;

use cxx_qt_lib::{QGuiApplication, QQmlApplicationEngine, QQuickStyle, QString, QUrl};
use cxx_qt_lib_extras::QApplication;
use std::env;

mod mdconverter;

fn main() {
    let mut app = 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
    if env::var("QT_QUICK_CONTROLS_STYLE").is_err() {
        QQuickStyle::set_style(&QString::from("org.kde.desktop"));
    }

    let mut engine = QQmlApplicationEngine::new();
    if let Some(engine) = engine.as_mut() {
        engine.load(&QUrl::from(
            "qrc:/qt/qml/org/kde/simplemdviewer/src/qml/Main.qml"
        ));
    }

    if let Some(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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
use cxx_qt_build::{CxxQtBuilder, QmlModule};

fn main() {
    CxxQtBuilder::new()
        .qml_module(QmlModule {
            uri: "org.kde.simplemdviewer",
            qml_files: &["src/qml/Main.qml"],
            rust_files: &["src/mdconverter.rs"],
            ..Default::default()
        })
        .build();
}

src/qml/Main.qml

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
import QtQuick
import QtQuick.Controls as Controls
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.simplemdviewer

Kirigami.ApplicationWindow {
    id: root

    title: "Simple Markdown Viewer in Rust 🦀"
    minimumWidth: Kirigami.Units.gridUnit * 20
    minimumHeight: Kirigami.Units.gridUnit * 20
    width: minimumWidth
    height: minimumHeight
    pageStack.initialPage: initPage

    Component {
        id: initPage

        Kirigami.Page {
            title: "Markdown Viewer"

            MdConverter {
                id: mdconverter

                sourceText: sourceArea.text
            }

            ColumnLayout {
                anchors {
                    top: parent.top
                    left: parent.left
                    right: parent.right
                }

                Controls.TextArea {
                    id: sourceArea

                    placeholderText: "Write some Markdown code here"
                    wrapMode: Text.WrapAnywhere
                    Layout.fillWidth: true
                    Layout.minimumHeight: Kirigami.Units.gridUnit * 5
                }

                RowLayout {
                    Layout.fillWidth: true

                    Controls.Button {
                        text: "Format"
                        onClicked: formattedText.text = mdconverter.mdFormat()
                    }

                    Controls.Button {
                        text: "Clear"
                        onClicked: {
                            sourceArea.text = "";
                            formattedText.text = "";
                        }
                    }

                }

                Controls.Label {
                    id: formattedText

                    textFormat: Text.RichText
                    wrapMode: Text.WordWrap
                    Layout.fillWidth: true
                    Layout.minimumHeight: Kirigami.Units.gridUnit * 5
                }

            }

        }

    }

}

Then the new QML type MdConverter that was exposed to QML needs to be instantiated:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
import QtQuick
import QtQuick.Controls as Controls
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.simplemdviewer

Kirigami.ApplicationWindow {
    id: root

    title: "Simple Markdown Viewer in Rust 🦀"
    minimumWidth: Kirigami.Units.gridUnit * 20
    minimumHeight: Kirigami.Units.gridUnit * 20
    width: minimumWidth
    height: minimumHeight
    pageStack.initialPage: initPage

    Component {
        id: initPage

        Kirigami.Page {
            title: "Markdown Viewer"

            MdConverter {
                id: mdconverter

                sourceText: sourceArea.text
            }

            ColumnLayout {
                anchors {
                    top: parent.top
                    left: parent.left
                    right: parent.right
                }

                Controls.TextArea {
                    id: sourceArea

                    placeholderText: "Write some Markdown code here"
                    wrapMode: Text.WrapAnywhere
                    Layout.fillWidth: true
                    Layout.minimumHeight: Kirigami.Units.gridUnit * 5
                }

                RowLayout {
                    Layout.fillWidth: true

                    Controls.Button {
                        text: "Format"
                        onClicked: formattedText.text = mdconverter.mdFormat()
                    }

                    Controls.Button {
                        text: "Clear"
                        onClicked: {
                            sourceArea.text = "";
                            formattedText.text = "";
                        }
                    }

                }

                Controls.Label {
                    id: formattedText

                    textFormat: Text.RichText
                    wrapMode: Text.WordWrap
                    Layout.fillWidth: true
                    Layout.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:

wget https://upload.wikimedia.org/wikipedia/commons/4/48/Markdown-mark.svg --output-document org.kde.simplemdviewer.svg

We need the icon to be perfectly squared, which can be accomplished with Inkscape:

  1. Open org.kde.simplemdviewer.svg in Inkscape.
  2. Type Ctrl+a to select everything.
  3. On the top W: text field, type 128 and press Enter.
  4. Go to File -> Document Properties...
  5. Change the Width: text field to 128 and press Enter.
  6. 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 Kirigami
Exec=simplemdviewer
Icon=org.kde.simplemdviewer
Type=Application
Terminal=false
Categories=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"?>
<component type="desktop-application">
    <id>org.kde.simplemdviewer</id>
    <launchable type="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>
        <release version="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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
cmake_minimum_required(VERSION 3.28)

project(simplemdviewer)

find_package(ECM 6.0 REQUIRED NO_MODULE)
set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH})
include(KDEInstallDirs)
include(ECMUninstallTarget)

include(ECMFindQmlModule)
ecm_find_qmlmodule(org.kde.kirigami REQUIRED)
find_package(KF6 REQUIRED COMPONENTS QQC2DesktopStyle)

add_custom_target(simplemdviewer
    ALL
    COMMAND cargo build --target-dir ${CMAKE_CURRENT_BINARY_DIR}
)

install(
    PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/debug/simplemdviewer
    DESTINATION ${KDE_INSTALL_BINDIR}
)

install(FILES org.kde.simplemdviewer.desktop DESTINATION ${KDE_INSTALL_APPDIR})
install(FILES org.kde.simplemdviewer.metainfo.xml DESTINATION ${KDE_INSTALL_METAINFODIR})
install(FILES org.kde.simplemdviewer.svg DESTINATION ${KDE_INSTALL_FULL_ICONDIR}/hicolor/scalable/apps)

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.

To learn more about where files need to be installed, see Building KDE software manually: The install step.

Final test run

At last, build, install and run your new application:

cmake -B build/ --install-prefix ~/.local
cmake --build build/
cmake --install build/
simplemdviewer

You can also try building it with Cargo:

cargo build --target-dir build/
cargo run --target-dir build/

You should now have a new menu entry named "Simple Markdown Viewer in Rust and Kirigami".

A screenshot of the Plasma Kickoff menu showing the menu entry.

Play with adding some Markdown text:

A screenshot of the application showing the custom window icon and the Free Software foundation definition of the four freedoms. Beneath it, the text is fully formatted in Markdown.

Hooray!

Our app so far

CMakeLists.txt
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
cmake_minimum_required(VERSION 3.28)

project(simplemdviewer)

find_package(ECM 6.0 REQUIRED NO_MODULE)
set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH})
include(KDEInstallDirs)
include(ECMUninstallTarget)

include(ECMFindQmlModule)
ecm_find_qmlmodule(org.kde.kirigami REQUIRED)
find_package(KF6 REQUIRED COMPONENTS QQC2DesktopStyle)

add_custom_target(simplemdviewer
    ALL
    COMMAND cargo build --target-dir ${CMAKE_CURRENT_BINARY_DIR}
)

install(
    PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/debug/simplemdviewer
    DESTINATION ${KDE_INSTALL_BINDIR}
)

install(FILES org.kde.simplemdviewer.desktop DESTINATION ${KDE_INSTALL_APPDIR})
install(FILES org.kde.simplemdviewer.metainfo.xml DESTINATION ${KDE_INSTALL_METAINFODIR})
install(FILES org.kde.simplemdviewer.svg DESTINATION ${KDE_INSTALL_FULL_ICONDIR}/hicolor/scalable/apps)
Cargo.toml
 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"
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" ] }
build.rs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
use cxx_qt_build::{CxxQtBuilder, QmlModule};

fn main() {
    CxxQtBuilder::new()
        .qml_module(QmlModule {
            uri: "org.kde.simplemdviewer",
            qml_files: &["src/qml/Main.qml"],
            rust_files: &["src/mdconverter.rs"],
            ..Default::default()
        })
        .build();
}
src/main.rs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// #[cxx_qt::bridge]
// mod ffi {
//     extern "RustQt" {
//         #[qobject]
//         type DummyQObject = super::DummyRustStruct;
//     }
// }
//
// #[derive(Default)]
// pub struct DummyRustStruct;

use cxx_qt_lib::{QGuiApplication, QQmlApplicationEngine, QQuickStyle, QString, QUrl};
use cxx_qt_lib_extras::QApplication;
use std::env;

mod mdconverter;

fn main() {
    let mut app = 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
    if env::var("QT_QUICK_CONTROLS_STYLE").is_err() {
        QQuickStyle::set_style(&QString::from("org.kde.desktop"));
    }

    let mut engine = QQmlApplicationEngine::new();
    if let Some(engine) = engine.as_mut() {
        engine.load(&QUrl::from(
            "qrc:/qt/qml/org/kde/simplemdviewer/src/qml/Main.qml"
        ));
    }

    if let Some(app) = app.as_mut() {
        app.exec();
    }
}
src/mdconverter.rs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
use std::pin::Pin;

use cxx_qt_lib::QString;

#[cxx_qt::bridge]
mod ffi {

    unsafe extern "C++" {
        include!("cxx-qt-lib/qstring.h");
        type QString = cxx_qt_lib::QString;
    }

    #[auto_cxx_name]
    unsafe extern "RustQt" {
        #[qobject]
        #[qml_element]
        #[qproperty(QString, source_text)]
        type MdConverter = super::MdConverterStruct;

        #[qinvokable]
        fn md_format(self: Pin<&mut MdConverter>) -> QString;
    }
}

#[derive(Default)]
pub struct MdConverterStruct {
    source_text: QString,
}

impl ffi::MdConverter {
    pub fn md_format(self: Pin<&mut Self>) -> QString {
        QString::from(&markdown::to_html(&self.source_text().to_string()))
    }
}
src/qml/Main.qml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
import QtQuick
import QtQuick.Controls as Controls
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.simplemdviewer

Kirigami.ApplicationWindow {
    id: root

    title: "Simple Markdown Viewer in Rust 🦀"
    minimumWidth: Kirigami.Units.gridUnit * 20
    minimumHeight: Kirigami.Units.gridUnit * 20
    width: minimumWidth
    height: minimumHeight
    pageStack.initialPage: initPage

    Component {
        id: initPage

        Kirigami.Page {
            title: "Markdown Viewer"

            MdConverter {
                id: mdconverter

                sourceText: sourceArea.text
            }

            ColumnLayout {
                anchors {
                    top: parent.top
                    left: parent.left
                    right: parent.right
                }

                Controls.TextArea {
                    id: sourceArea

                    placeholderText: "Write some Markdown code here"
                    wrapMode: Text.WrapAnywhere
                    Layout.fillWidth: true
                    Layout.minimumHeight: Kirigami.Units.gridUnit * 5
                }

                RowLayout {
                    Layout.fillWidth: true

                    Controls.Button {
                        text: "Format"
                        onClicked: formattedText.text = mdconverter.mdFormat()
                    }

                    Controls.Button {
                        text: "Clear"
                        onClicked: {
                            sourceArea.text = "";
                            formattedText.text = "";
                        }
                    }

                }

                Controls.Label {
                    id: formattedText

                    textFormat: Text.RichText
                    wrapMode: Text.WordWrap
                    Layout.fillWidth: true
                    Layout.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 Kirigami
Exec=simplemdviewer
Icon=org.kde.simplemdviewer
Type=Application
Terminal=false
Categories=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"?>
<component type="desktop-application">
    <id>org.kde.simplemdviewer</id>
    <launchable type="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>
        <release version="0.1.0" date="2025-07-01"/>
    </releases>
</component>