-
-
Notifications
You must be signed in to change notification settings - Fork 2
Scripting
Automate PlasmaZones with shell scripts, Python, or any D-Bus client. This guide covers practical scripting patterns for layout switching, window management, multi-monitor setups, and event-driven automation.
For the full API reference, see the D-Bus API pages.
| Property | Value |
|---|---|
| Bus | Session bus (never system bus) |
| Service name | org.plasmazones |
| Object path | /PlasmaZones |
| Interfaces |
LayoutManager, Overlay, Screen, Settings, WindowDrag, WindowTracking
|
| Tool | Best for | Variant support | Signal monitoring | Availability |
|---|---|---|---|---|
| qdbus6 | Quick one-off calls | Yes | No | KDE/Qt6 |
| gdbus | Full-featured scripting | Yes (GVariant) | Yes | All Linux (glib) |
| busctl | Introspection, debugging | Yes | Yes | All systemd |
| dbus-send | Simple calls, minimal deps | No variants | No | Universal |
| Python (dasbus) | Complex automation | Yes | Yes (event loop) | pip install |
Recommendation: Use gdbus for shell scripts (handles all types, signal monitoring, universally available). Use qdbus6 for quick interactive calls. Use dasbus for Python automation.
# Any of these work:
busctl --user status org.plasmazones 2>/dev/null && echo "Running" || echo "Not running"
qdbus6 org.freedesktop.DBus /org/freedesktop/DBus org.freedesktop.DBus.NameHasOwner org.plasmazones
gdbus call --session --dest org.freedesktop.DBus \
--object-path /org/freedesktop/DBus \
--method org.freedesktop.DBus.NameHasOwner "org.plasmazones"qdbus6 org.plasmazones /PlasmaZones org.plasmazones.LayoutManager.getLayoutListqdbus6 org.plasmazones /PlasmaZones org.plasmazones.LayoutManager.getActiveLayoutqdbus6 org.plasmazones /PlasmaZones org.plasmazones.LayoutManager.setActiveLayout "{uuid}"qdbus6 org.plasmazones /PlasmaZones org.plasmazones.Overlay.showOverlay
qdbus6 org.plasmazones /PlasmaZones org.plasmazones.Overlay.hideOverlay
qdbus6 org.plasmazones /PlasmaZones org.plasmazones.Overlay.toggleOverlayqdbus6 org.plasmazones /PlasmaZones org.plasmazones.Screen.getScreensgdbus is the recommended tool for shell scripts because it handles all D-Bus types (including variants needed for settings) and has built-in signal monitoring.
# No arguments
gdbus call --session --dest org.plasmazones \
--object-path /PlasmaZones \
--method org.plasmazones.LayoutManager.getLayoutList
# String argument
gdbus call --session --dest org.plasmazones \
--object-path /PlasmaZones \
--method org.plasmazones.LayoutManager.setActiveLayout \
"a1b2c3d4-e5f6-7890-abcd-ef1234567890"
# Multiple arguments (int + string)
gdbus call --session --dest org.plasmazones \
--object-path /PlasmaZones \
--method org.plasmazones.LayoutManager.applyQuickLayout \
1 "DP-1"
# String + int + string
gdbus call --session --dest org.plasmazones \
--object-path /PlasmaZones \
--method org.plasmazones.LayoutManager.assignLayoutToScreenDesktop \
"DP-1" 2 "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
# Boolean argument
gdbus call --session --dest org.plasmazones \
--object-path /PlasmaZones \
--method org.plasmazones.LayoutManager.setLayoutHidden \
"a1b2c3d4-e5f6-7890-abcd-ef1234567890" true
# Variant argument (for settings)
gdbus call --session --dest org.plasmazones \
--object-path /PlasmaZones \
--method org.plasmazones.Settings.setSetting \
"zonePadding" "<int32 12>"
gdbus call --session --dest org.plasmazones \
--object-path /PlasmaZones \
--method org.plasmazones.Settings.setSetting \
"showZoneNumbers" "<true>"
# Dict argument
gdbus call --session --dest org.plasmazones \
--object-path /PlasmaZones \
--method org.plasmazones.LayoutManager.setAllScreenAssignments \
"{'DP-1': 'uuid-layout-1', 'HDMI-1': 'uuid-layout-2'}"Most methods return JSON strings. gdbus wraps them in GVariant tuples like ('json...',). Strip the wrapper with sed, then parse with jq:
# Pretty-print the active layout
gdbus call --session --dest org.plasmazones --object-path /PlasmaZones \
--method org.plasmazones.LayoutManager.getActiveLayout | \
sed "s/^('//; s/',)$//" | jq .
# Get layout name
gdbus call --session --dest org.plasmazones --object-path /PlasmaZones \
--method org.plasmazones.LayoutManager.getActiveLayout | \
sed "s/^('//; s/',)$//" | jq -r '.name'
# Count zones
gdbus call --session --dest org.plasmazones --object-path /PlasmaZones \
--method org.plasmazones.LayoutManager.getActiveLayout | \
sed "s/^('//; s/',)$//" | jq '.zones | length'
# Pretty-print all settings
gdbus call --session --dest org.plasmazones --object-path /PlasmaZones \
--method org.plasmazones.Settings.getAllSettings | \
sed "s/^('//; s/',)$//" | jq .#!/usr/bin/env bash
set -euo pipefail
SERVICE="org.plasmazones"
OBJECT="/PlasmaZones"
# Call a PlasmaZones method and return the raw string result
pz_call() {
local method="$1"; shift
local raw
raw=$(gdbus call --session --dest "$SERVICE" --object-path "$OBJECT" --method "$method" "$@")
# Strip GVariant tuple wrapper: ('value',) -> value
echo "$raw" | sed "s/^('//; s/',)$//"
}
# Example usage:
active=$(pz_call "org.plasmazones.LayoutManager.getActiveLayout")
echo "Active: $(echo "$active" | jq -r '.name')"Some systems have qdbus6, others have qdbus:
QDBUS=$(command -v qdbus6 2>/dev/null || command -v qdbus 2>/dev/null)
if [ -z "$QDBUS" ]; then
echo "Error: qdbus6/qdbus not found. Install qt6-tools." >&2
exit 1
fi
$QDBUS org.plasmazones /PlasmaZones org.plasmazones.LayoutManager.getLayoutListdbus-send is the most portable option but cannot handle variant types (needed for setSetting). Use it for simple calls:
# No arguments
dbus-send --session --print-reply --dest=org.plasmazones \
/PlasmaZones \
org.plasmazones.LayoutManager.getLayoutList
# String argument
dbus-send --session --print-reply --dest=org.plasmazones \
/PlasmaZones \
org.plasmazones.LayoutManager.setActiveLayout \
string:"a1b2c3d4-e5f6-7890-abcd-ef1234567890"
# Multiple arguments (int + string)
dbus-send --session --print-reply --dest=org.plasmazones \
/PlasmaZones \
org.plasmazones.LayoutManager.applyQuickLayout \
int32:1 string:"DP-1"
# Boolean argument
dbus-send --session --print-reply --dest=org.plasmazones \
/PlasmaZones \
org.plasmazones.LayoutManager.setLayoutHidden \
string:"uuid-here" boolean:truebusctl is available on all systemd systems and has excellent introspection:
# Introspect all methods and signals
busctl --user introspect org.plasmazones /PlasmaZones
# Call with type signature (s = string, i = int32, b = boolean)
busctl --user call org.plasmazones /PlasmaZones \
org.plasmazones.LayoutManager getLayoutList
busctl --user call org.plasmazones /PlasmaZones \
org.plasmazones.LayoutManager getLayout s "uuid-here"
busctl --user call org.plasmazones /PlasmaZones \
org.plasmazones.LayoutManager applyQuickLayout is 1 "DP-1"
# Monitor all PlasmaZones signals
busctl --user monitor org.plasmazones# All PlasmaZones signals
gdbus monitor --session --dest org.plasmazones --object-path /PlasmaZones
# Filter with grep
gdbus monitor --session --dest org.plasmazones --object-path /PlasmaZones 2>&1 | \
grep -E 'layoutChanged|screenLayoutChanged|overlayVisibilityChanged'# All signals from PlasmaZones
dbus-monitor --session "type='signal',sender='org.plasmazones'"
# Specific interface
dbus-monitor --session "type='signal',interface='org.plasmazones.LayoutManager'"
# Specific signal
dbus-monitor --session "type='signal',interface='org.plasmazones.LayoutManager',member='layoutChanged'"
# Debug: watch method calls TO PlasmaZones
dbus-monitor --session "type='method_call',destination='org.plasmazones'"| Signal | Interface | Parameters | When emitted |
|---|---|---|---|
layoutChanged |
LayoutManager | layoutJson | Active layout modified |
layoutListChanged |
LayoutManager | — | Layout created/deleted/imported |
screenLayoutChanged |
LayoutManager | screenName, layoutId | Screen assignment changed |
overlayVisibilityChanged |
Overlay | visible | Overlay shown/hidden |
settingsChanged |
Settings | — | Any setting modified |
screenAdded |
Screen | screenName | Monitor connected |
screenRemoved |
Screen | screenName | Monitor disconnected |
screenGeometryChanged |
Screen | screenName | Resolution/position changed |
virtualDesktopCountChanged |
LayoutManager | count | Desktop added/removed |
currentActivityChanged |
LayoutManager | activityId | KDE Activity switched |
navigationFeedback |
WindowTracking | success, action, reason, sourceZone, targetZone, screen | Keyboard navigation result |
pip install dasbus#!/usr/bin/env python3
"""PlasmaZones automation with dasbus."""
import json
from dasbus.connection import SessionMessageBus
from dasbus.loop import EventLoop
SERVICE = "org.plasmazones"
OBJECT_PATH = "/PlasmaZones"
bus = SessionMessageBus()
# Get interface proxies
layouts = bus.get_proxy(SERVICE, OBJECT_PATH, "org.plasmazones.LayoutManager")
overlay = bus.get_proxy(SERVICE, OBJECT_PATH, "org.plasmazones.Overlay")
settings = bus.get_proxy(SERVICE, OBJECT_PATH, "org.plasmazones.Settings")
screens = bus.get_proxy(SERVICE, OBJECT_PATH, "org.plasmazones.Screen")
# Query layouts
layout_ids = layouts.getLayoutList()
print(f"Found {len(layout_ids)} layouts")
active = json.loads(layouts.getActiveLayout())
print(f"Active: {active['name']} ({len(active.get('zones', []))} zones)")
# Switch layout
layouts.setActiveLayout(layout_ids[0])
# Query screens
for name in screens.getScreens():
info = json.loads(screens.getScreenInfo(name))
print(f"Screen {name}: {info['geometry']['width']}x{info['geometry']['height']}")
# Show overlay briefly
overlay.showOverlay()
import time; time.sleep(2)
overlay.hideOverlay()
# Subscribe to signals
def on_layout_changed(layout_json):
data = json.loads(layout_json)
print(f"Layout changed: {data['name']}")
def on_screen_added(screen_name):
print(f"Screen connected: {screen_name}")
layouts.layoutChanged.connect(on_layout_changed)
screens_proxy = bus.get_proxy(SERVICE, OBJECT_PATH, "org.plasmazones.Screen")
# Note: screen signals require the Screen interface proxy
print("Listening for signals (Ctrl+C to stop)...")
loop = EventLoop()
loop.run()pip install pydbus#!/usr/bin/env python3
"""PlasmaZones automation with pydbus."""
import json
from pydbus import SessionBus
from gi.repository import GLib
bus = SessionBus()
pz = bus.get("org.plasmazones", "/PlasmaZones")
# Methods from all interfaces are merged into one proxy
layouts = pz.getLayoutList()
active = json.loads(pz.getActiveLayout())
print(f"Active: {active['name']}")
# Subscribe to signals
pz.layoutChanged.connect(lambda j: print("Changed:", json.loads(j)["name"]))
pz.overlayVisibilityChanged.connect(lambda v: print("Overlay:", v))
loop = GLib.MainLoop()
loop.run()pip install dbus-python
# Or: apt install python3-dbus / dnf install python3-dbus#!/usr/bin/env python3
"""PlasmaZones automation with dbus-python."""
import dbus
import dbus.mainloop.glib
from gi.repository import GLib
import json
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
bus = dbus.SessionBus()
obj = bus.get_object("org.plasmazones", "/PlasmaZones")
layout_iface = dbus.Interface(obj, "org.plasmazones.LayoutManager")
overlay_iface = dbus.Interface(obj, "org.plasmazones.Overlay")
# Call methods
layouts = list(layout_iface.getLayoutList())
active = json.loads(str(layout_iface.getActiveLayout()))
print(f"Active: {active['name']}")
# Subscribe to signals
bus.add_signal_receiver(
lambda j: print("Changed:", json.loads(str(j))["name"]),
dbus_interface="org.plasmazones.LayoutManager",
signal_name="layoutChanged"
)
GLib.MainLoop().run()#!/usr/bin/env bash
set -euo pipefail
IFACE="org.plasmazones.LayoutManager"
pz() {
gdbus call --session --dest org.plasmazones --object-path /PlasmaZones --method "$@"
}
strip() { sed "s/^('//; s/',)$//"; }
current_id=$(pz "$IFACE.getActiveLayout" | strip | jq -r '.id')
mapfile -t ids < <(pz "$IFACE.getLayoutList" | grep -oP "'[^']+'" | tr -d "'")
for i in "${!ids[@]}"; do
if [ "${ids[$i]}" = "$current_id" ]; then
next=$(( (i + 1) % ${#ids[@]} ))
pz "$IFACE.setActiveLayout" "${ids[$next]}" > /dev/null
echo "Switched to: ${ids[$next]}"
exit 0
fi
done#!/usr/bin/env bash
set -euo pipefail
OUTPUT_DIR="${1:-$HOME/plasmazones-backups}"
mkdir -p "$OUTPUT_DIR"
IFACE="org.plasmazones.LayoutManager"
pz() {
gdbus call --session --dest org.plasmazones --object-path /PlasmaZones --method "$@"
}
strip() { sed "s/^('//; s/',)$//"; }
mapfile -t ids < <(pz "$IFACE.getLayoutList" | grep -oP "'[^']+'" | tr -d "'")
for id in "${ids[@]}"; do
[ -z "$id" ] && continue
json=$(pz "$IFACE.getLayout" "$id" | strip)
name=$(echo "$json" | jq -r '.name // "unnamed"' | tr ' /' '_-')
echo "$json" | jq '.' > "$OUTPUT_DIR/${name}_${id}.json"
echo "Exported: $name"
done
echo "Done. ${#ids[@]} layouts exported to $OUTPUT_DIR"#!/usr/bin/env bash
# Watches for screen add/remove and assigns layouts
set -euo pipefail
LAYOUT_FOR_DP1="uuid-for-dp1-layout"
LAYOUT_FOR_HDMI="uuid-for-hdmi-layout"
echo "Watching for screen changes..."
gdbus monitor --session --dest org.plasmazones --object-path /PlasmaZones 2>&1 | \
while IFS= read -r line; do
if echo "$line" | grep -q "screenAdded"; then
screen=$(echo "$line" | grep -oP "'[^']+'" | tail -1 | tr -d "'")
echo "Screen added: $screen"
case "$screen" in
DP-*)
gdbus call --session --dest org.plasmazones --object-path /PlasmaZones \
--method org.plasmazones.LayoutManager.assignLayoutToScreen \
"$screen" "$LAYOUT_FOR_DP1" > /dev/null
echo " Assigned DP layout"
;;
HDMI-*)
gdbus call --session --dest org.plasmazones --object-path /PlasmaZones \
--method org.plasmazones.LayoutManager.assignLayoutToScreen \
"$screen" "$LAYOUT_FOR_HDMI" > /dev/null
echo " Assigned HDMI layout"
;;
esac
elif echo "$line" | grep -q "screenRemoved"; then
screen=$(echo "$line" | grep -oP "'[^']+'" | tail -1 | tr -d "'")
echo "Screen removed: $screen"
fi
done#!/usr/bin/env bash
set -euo pipefail
LOGFILE="${1:-$HOME/plasmazones-events.log}"
echo "Logging PlasmaZones events to $LOGFILE (Ctrl+C to stop)..."
gdbus monitor --session --dest org.plasmazones --object-path /PlasmaZones 2>&1 | \
while IFS= read -r line; do
ts=$(date +"%Y-%m-%d %H:%M:%S")
if echo "$line" | grep -q "layoutChanged"; then
echo "[$ts] Layout modified" >> "$LOGFILE"
elif echo "$line" | grep -q "layoutListChanged"; then
echo "[$ts] Layout list updated" >> "$LOGFILE"
elif echo "$line" | grep -q "screenLayoutChanged"; then
echo "[$ts] Screen assignment changed: $line" >> "$LOGFILE"
elif echo "$line" | grep -q "overlayVisibilityChanged"; then
echo "[$ts] Overlay toggled" >> "$LOGFILE"
elif echo "$line" | grep -q "settingsChanged"; then
echo "[$ts] Settings changed" >> "$LOGFILE"
elif echo "$line" | grep -q "screenAdded"; then
echo "[$ts] Screen connected" >> "$LOGFILE"
elif echo "$line" | grep -q "screenRemoved"; then
echo "[$ts] Screen disconnected" >> "$LOGFILE"
elif echo "$line" | grep -q "navigationFeedback"; then
echo "[$ts] Navigation: $line" >> "$LOGFILE"
fi
done#!/usr/bin/env bash
set -euo pipefail
IFACE_LAYOUT="org.plasmazones.LayoutManager"
IFACE_SCREEN="org.plasmazones.Screen"
pz() {
gdbus call --session --dest org.plasmazones --object-path /PlasmaZones --method "$@"
}
strip() { sed "s/^('//; s/',)$//"; }
echo "=== Screens ==="
screens_raw=$(pz "$IFACE_SCREEN.getScreens")
screens=$(echo "$screens_raw" | grep -oP "'[^']+'" | tr -d "'")
while IFS= read -r screen; do
[ -z "$screen" ] && continue
info=$(pz "$IFACE_SCREEN.getScreenInfo" "$screen" | strip)
res=$(echo "$info" | jq -r '"\(.geometry.width)x\(.geometry.height)"')
echo " $screen: $res"
layout_id=$(pz "$IFACE_LAYOUT.getLayoutForScreen" "$screen" | strip)
if [ -n "$layout_id" ] && [ "$layout_id" != "" ]; then
layout=$(pz "$IFACE_LAYOUT.getLayout" "$layout_id" | strip)
name=$(echo "$layout" | jq -r '.name // "unnamed"')
zones=$(echo "$layout" | jq '.zones | length')
echo " Layout: $name ($zones zones)"
else
echo " Layout: (default)"
fi
done <<< "$screens"
echo ""
echo "=== Active Layout ==="
active=$(pz "$IFACE_LAYOUT.getActiveLayout" | strip)
echo " Name: $(echo "$active" | jq -r '.name')"
echo " Zones: $(echo "$active" | jq '.zones | length')"
echo "$active" | jq -r '.zones[] | " [\(.zoneNumber)] \(.relativeGeometry | "\(.x),\(.y) \(.width)x\(.height)")"'#!/usr/bin/env python3
"""Auto-switch layouts when KDE Activities change."""
import json
from dasbus.connection import SessionMessageBus
from dasbus.loop import EventLoop
ACTIVITY_LAYOUTS = {
"work-activity-uuid": "work-layout-uuid",
"gaming-activity-uuid": "gaming-layout-uuid",
"media-activity-uuid": "media-layout-uuid",
}
bus = SessionMessageBus()
layouts = bus.get_proxy("org.plasmazones", "/PlasmaZones",
"org.plasmazones.LayoutManager")
screens = bus.get_proxy("org.plasmazones", "/PlasmaZones",
"org.plasmazones.Screen")
def on_activity_changed(activity_id):
layout_id = ACTIVITY_LAYOUTS.get(activity_id)
if layout_id:
for screen in screens.getScreens():
layouts.assignLayoutToScreenActivity(screen, activity_id, layout_id)
print(f"Activity {activity_id} -> layout {layout_id}")
layouts.currentActivityChanged.connect(on_activity_changed)
print("Watching for activity changes...")
EventLoop().run()| Method | Args | Returns | Description |
|---|---|---|---|
getLayoutList() |
— | string[] | All layout UUIDs |
getActiveLayout() |
— | JSON | Active layout with zones |
getLayout(id) |
string | JSON | Layout by UUID |
setActiveLayout(id) |
string | — | Switch active layout |
createLayout(name, type) |
string, string | layoutId | Create empty layout |
deleteLayout(id) |
string | — | Delete layout |
duplicateLayout(id) |
string | layoutId | Copy layout |
importLayout(filePath) |
string | layoutId | Import from file |
exportLayout(id, filePath) |
string, string | — | Export to file |
applyQuickLayout(slot, screen) |
int, string | — | Apply quick slot 1-9 |
| Method | Args | Returns | Description |
|---|---|---|---|
getLayoutForScreen(screen) |
string | layoutId | Layout active on screen |
assignLayoutToScreen(screen, layout) |
string, string | — | Assign base layout |
clearAssignment(screen) |
string | — | Remove assignment |
getAllScreenAssignments() |
— | JSON | All screen mappings |
setAllScreenAssignments(map) |
dict | — | Batch assign |
| Method | Args | Returns | Description |
|---|---|---|---|
assignLayoutToScreenDesktop(screen, desktop, layout) |
string, int, string | — | Per-desktop layout |
getLayoutForScreenDesktop(screen, desktop) |
string, int | layoutId | Query per-desktop |
clearAssignmentForScreenDesktop(screen, desktop) |
string, int | — | Remove override |
getVirtualDesktopCount() |
— | int | Number of desktops |
getVirtualDesktopNames() |
— | string[] | Desktop names |
| Method | Args | Returns | Description |
|---|---|---|---|
showOverlay() |
— | — | Show zone overlay |
hideOverlay() |
— | — | Hide overlay |
toggleOverlay() |
— | — | Toggle visibility |
isOverlayVisible() |
— | bool | Query visibility |
highlightZone(zoneId) |
string | — | Highlight one zone |
clearHighlight() |
— | — | Clear highlights |
detectZoneAtPosition(x, y) |
int, int | zoneId | Zone at coordinates |
| Method | Args | Returns | Description |
|---|---|---|---|
getScreens() |
— | string[] | All screen names |
getScreenInfo(screen) |
string | JSON | Screen details |
getScreenId(screen) |
string | string | Stable EDID-based ID |
| Method | Args | Returns | Description |
|---|---|---|---|
getAllSettings() |
— | JSON | All settings |
getSetting(key) |
string | variant | Single setting |
setSetting(key, value) |
string, variant | bool | Set setting |
getSettingKeys() |
— | string[] | Available keys |
saveSettings() |
— | — | Save to disk |
reloadSettings() |
— | — | Reload from disk |
resetToDefaults() |
— | — | Factory reset |
| Method | Args | Returns | Description |
|---|---|---|---|
getSnappedWindows() |
— | string[] | All snapped window IDs |
getZoneForWindow(windowId) |
string | zoneId | Zone a window is in |
getWindowsInZone(zoneId) |
string | string[] | Windows in a zone |
isWindowFloating(windowId) |
string | bool | Check floating state |
setWindowFloating(windowId, floating) |
string, bool | — | Toggle floating |
getZoneGeometry(zoneId) |
string | JSON | Zone bounds |
getZoneGeometryForScreen(zoneId, screen) |
string, string | JSON | Zone bounds on screen |
All methods are on the org.plasmazones.* interfaces. Prefix method names with the full interface when calling (e.g., org.plasmazones.LayoutManager.getLayoutList).
-
Session bus only. PlasmaZones always uses the session bus (
--session/SessionBus()). Never use--system. -
Single object path. All interfaces share
/PlasmaZones. Specify the interface when calling methods. -
JSON everywhere. Complex data is returned as JSON strings. Use
jqin bash orjson.loads()in Python. -
Wayland cursor. On Wayland, background processes cannot read cursor position reliably. The KWin effect reports cursor data to the daemon. Scripts that need cursor info should use
detectZoneAtPosition()with coordinates from another source. -
qdbus6 vs qdbus. Plasma 6 ships
qdbus6. Some distros symlinkqdbusto it; check both in scripts.