Plugin Creation Guide
July 10, 2026 · View on GitHub
Rule #1
- A plugin always lives in its own dedicated folder.
Rule #2
- A plugin must not consume significant device resources. Any language is allowed for performance reasons, but Go is recommended.
Rule #3
- File names inside a plugin should briefly describe its purpose. The file to be loaded is specified in the plugin's installation instructions.
- If a plugin has complex functionality in a separate window, that window must be wrapped in a
lazyLoader.
Visual style
- For the main background of a plugin, use:
Rectangle {
opacity: 0.85
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop { position: 0.0; color: col.background3 }
GradientStop { position: 0.05; color: col.background2 }
GradientStop { position: 0.3; color: col.background1 }
GradientStop { position: 0.7; color: col.background1 }
GradientStop { position: 0.95; color: col.background2 }
GradientStop { position: 1.0; color: col.background3 }
}
}
- For button backgrounds and similar elements:
Rectangle {
opacity: 0.65
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop { position: 0.0; color: col.backgroundAlt2 }
GradientStop { position: 0.275; color: col.backgroundAlt1 }
GradientStop { position: 0.725; color: col.backgroundAlt1 }
GradientStop { position: 1.0; color: col.backgroundAlt2 }
}
}
- For hover effects, use:
Item {
id: button
property bool hovered: false
Rectangle {
anchors.fill: parent
radius: mainRad - 3
opacity: 0.65
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop { position: 0.0; color: col.backgroundAlt2 }
GradientStop { position: 0.275; color: col.backgroundAlt1 }
GradientStop { position: 0.725; color: col.backgroundAlt1 }
GradientStop { position: 1.0; color: col.backgroundAlt2 }
}
}
Rectangle {
anchors.fill: parent
anchors.margins: 2
radius: mainRad - 5 // sum up all margins
color: button.hovered ? col.accent : "transparent"
Behavior on color { ColorAnimation { duration: 200 } }
}
// code
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: {
button.hovered = true
}
onExited: {
button.hovered = false
}
}
}
A different background can be used — the example above used the button background style.
- For radii, use
radius: mainRad. If you apply margins, writeradius: mainRad - <margin_value>in the inner block. - All colors must come from the global
colobject (defined incolors.jsonand available viashell.qml). - JES also supports base16 themes (
base.base<01-16>). - Font is set via fontFamily and fontSize.
- JES has 2 accent colors — dark and light.
Passing data to the interface
- Use
JsonListenfor a continuous stream (recommended for performance), andJsonPollfor a one-time request on a fixed interval. - Data is passed as JSON. For visual-only programs with no logic (e.g. cava in the bar), a plain string is sufficient.
- Window Manager data is passed via the
barparameter. If you need data about coordinates/workspaces/active program/layout – callbar. For a list of available data, seeBaseBar.qml.
If anything is unclear, refer to BaseBar.qml in the bar/ folder — it is the visual reference for all UI.
Connecting to the JES launcher
- To connect to the launcher, we call the following function:
property var api: launchLoader ? launchLoader.item : null
function ensureTab() {
if (!api) return
var exists = false
for (var i = 0; i < api.tabModel.length; i++) {
if (api.tabModel[i].name === "Tab Name") {
exists = true
break
}
}
if (!exists) {
api.tabModel.push({
name: "Tab Name",
icon: "Icon for search, use only from Nerd Font",
placeholder: "Enter text...",
info: []
})
}
}
onApiChanged: {
if (api && launchLoader && launchLoader.active) {
ensureTab()
firstOpen = false
}
}
-
In
infowe can pass any list containing the following items:{"id", "name", "icon", "exec"}– this is a pseudo‑JSON, just names for objects. -
In
idwe pass the serial number. -
In
namethe text that will be displayed in the block. -
In
iconthe icon, if any. -
In
execthe command to be executed.
id is optional if you specify full commands for the object. It is required if you created a script that should run different objects.
Connecting to the JES plugin center
- To connect to the plugin center, we call the following function:
property var api: pluginPopupLoader ? pluginPopupLoader.item : null
function ensurePlugins() {
if (!api) return
var modules = [
{ source: Qt.resolvedUrl("Content.qml"), colSpan: 1, rowSpan: 1 }
]
for (var i = 0; i < modules.length; i++) {
var mod = modules[i]
var exists = false
for (var j = 0; j < api.pluginInfo.length; j++) {
if (api.pluginInfo[j].source === mod.source) {
exists = true
break
}
}
if (!exists) {
api.pluginInfo.push(mod)
console.log("[ExamplePlugin] Added module:", mod.source)
}
}
}
onApiChanged: {
ensurePlugins()
}
- Maximum dimensions:
colSpan: 3, rowSpan: 7 - Any module can be passed in
source.