'Plasmoid' is a fancy name for a KDE Plasma widget. System tray, clock, application launcher - these are all plasmoids shipped with Plasma.
I recently created one and hit several roadblocks with the official documentation. Here are my notes on the implicit behaviors and non-obvious aspects I discovered.
Plasma has strict expectations about file structure. Deviating from this pattern leads to mysterious failures:
The metadata.json file defines the plasmoid's identity and behavior:
{
"KPlugin": {
"Authors": [
{
"Email": "contact@example.com",
"Name": "Your Name"
}
],
"Category": "Utilities",
"Description": "A simple Hello World plasmoid",
"Icon": "document-text",
"Id": "com.example.helloworld",
"Name": "Hello World",
"Version": "1.0"
},
"X-Plasma-API": "declarativeappletscript",
"X-Plasma-MainScript": "ui/main.qml",
"KPackageStructure": "Plasma/Applet"
}
Things that tripped me up:
KPackageStructure dictates the expected file structureX-Plasma-MainScript is relative to the contents/ directory, not the package/ directoryId becomes the installation directory name under ~/.local/share/plasma/plasmoids/QML powers the UI of plasmoids. The main.qml file serves as the entry point:
import QtQuick 2.0
import org.kde.plasma.plasmoid 2.0
import org.kde.plasma.core 2.0 as PlasmaCore
Item {
id: root
property string greeting: "Hello, Plasma!"
Plasmoid.fullRepresentation: Rectangle {
width: 200
height: 100
color: PlasmaCore.Theme.backgroundColor
Text {
anchors.centerIn: parent
text: root.greeting
color: PlasmaCore.Theme.textColor
}
}
}
Using PlasmaCore.Theme properties automatically adapts your plasmoid to the user's color scheme, which is pretty handy.
Plasmoids have two display modes: compact (for panels) and full (for expanded views or desktop widgets).
For panels, you need a compact representation:
import QtQuick 2.0
import org.kde.plasma.core 2.0 as PlasmaCore
Item {
id: compactRoot
width: 24
height: 24
Rectangle {
anchors.fill: parent
color: PlasmaCore.Theme.highlightColor
Text {
anchors.centerIn: parent
text: "HW"
color: PlasmaCore.Theme.highlightedTextColor
font.pixelSize: parent.height * 0.6
}
}
MouseArea {
anchors.fill: parent
onClicked: plasmoid.expanded = !plasmoid.expanded
}
}
For the expanded view, you use a full representation:
import QtQuick 2.0
import org.kde.plasma.core 2.0 as PlasmaCore
Rectangle {
id: fullRoot
width: 200
height: 100
color: PlasmaCore.Theme.backgroundColor
Text {
anchors.centerIn: parent
text: plasmoid.rootItem.greeting
color: PlasmaCore.Theme.textColor
}
}
When splitting these into separate files, the main.qml references them:
import QtQuick 2.0
import org.kde.plasma.plasmoid 2.0
import org.kde.plasma.core 2.0 as PlasmaCore
Item {
id: root
property string greeting: "Hello, Plasma!"
Plasmoid.compactRepresentation: CompactRepresentation {}
Plasmoid.fullRepresentation: FullRepresentation {}
}
I discovered that these files can access data from main.qml through plasmoid.rootItem - useful for sharing variables between different views.
The configuration system involves three files working together:
The main.xml file defines what settings you want to save:
<?xml version="1.0" encoding="UTF-8"?>
<kcfg xmlns="http://www.kde.org/standards/kcfg/1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.kde.org/standards/kcfg/1.0
http://www.kde.org/standards/kcfg/1.0/kcfg.xsd" >
<kcfgfile name=""/>
<group name="General">
<entry name="customGreeting" type="String">
<default>Hello, Plasma!</default>
</entry>
</group>
</kcfg>
The config.qml file defines how the settings UI is organized:
import QtQuick 2.0
import org.kde.plasma.configuration 2.0
ConfigModel {
ConfigCategory {
name: "General"
icon: "configure"
source: "configGeneral.qml"
}
}
Something weird: source points to contents/ui/configGeneral.qml, not to contents/config/configGeneral.qml as you might expect.
The configGeneral.qml file is the actual settings form:
import QtQuick 2.0
import QtQuick.Controls 2.5
import QtQuick.Layouts 1.12
Item {
id: configPage
property alias cfg_customGreeting: greetingField.text
ColumnLayout {
anchors.left: parent.left
anchors.right: parent.right
Label {
text: "Custom greeting:"
}
TextField {
id: greetingField
Layout.fillWidth: true
}
}
}
The naming convention cfg_ + property name is how KDE knows which properties to save. When you edit the text field, it automatically saves the value - no extra code needed.
In main.qml, you access saved settings with:
property string greeting: plasmoid.configuration.customGreeting
The values are stored in ~/.config/plasma-org.kde.plasma.desktop-appletsrc, but you never need to touch this file directly.
The plasmoidviewer tool is great for testing:
# View full representation
plasmoidviewer -a ~/Code/plasmoid-helloworld/package/
# View compact representation (as in panel)
plasmoidviewer -f horizontal -a ~/Code/plasmoid-helloworld/package/
# Test in a panel container
plasmoidviewer -c org.kde.panel -a ~/Code/plasmoid-helloworld/package/
To install, you copy the package directory to ~/.local/share/plasma/plasmoids/<id>:
# First-time installation
plasmapkg2 -t Plasma/Applet -i ~/Code/plasmoid-helloworld/package/
# Update existing installation
plasmapkg2 -t Plasma/Applet -u ~/Code/plasmoid-helloworld/package/
You'll need to log out and back in before the plasmoid shows up in the widget selection menu.
QML has different types of layout elements:
Containers organize and arrange elements:
Item: Invisible container (the basic one)Rectangle: Visible container with color, border, etc.Column, Row, and Grid: Simple layout containersColumnLayout, RowLayout, and GridLayout: More powerful layout containersViews display collections of data:
ListView: Shows items in a listGridView: Shows items in a gridPathView: Shows items along a pathRepeater: Simple way to create multiple copies of an itemI found the code from plasma-applet-eventcalendar more helpful than the official docs in many cases.
I've also had good luck asking LLMs for QML examples when I get stuck.
A few weeks after writing this quicknote I upgraded to Plasma 6 and you guessed it: my widget does not work...
I thought I was smart taking notes for later in case I needed to do it again, turns out I will need to re-learn from scratch anyway...