Skip to content

Scripting

Nathan edited this page Feb 22, 2026 · 1 revision

Scripting Guide

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.


Service Details

Property Value
Bus Session bus (never system bus)
Service name org.plasmazones
Object path /PlasmaZones
Interfaces LayoutManager, Overlay, Screen, Settings, WindowDrag, WindowTracking

Choosing a Tool

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.


Quick Start

Check if the daemon is running

# 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"

List layouts

qdbus6 org.plasmazones /PlasmaZones org.plasmazones.LayoutManager.getLayoutList

Get active layout (JSON)

qdbus6 org.plasmazones /PlasmaZones org.plasmazones.LayoutManager.getActiveLayout

Switch layout

qdbus6 org.plasmazones /PlasmaZones org.plasmazones.LayoutManager.setActiveLayout "{uuid}"

Show/hide overlay

qdbus6 org.plasmazones /PlasmaZones org.plasmazones.Overlay.showOverlay
qdbus6 org.plasmazones /PlasmaZones org.plasmazones.Overlay.hideOverlay
qdbus6 org.plasmazones /PlasmaZones org.plasmazones.Overlay.toggleOverlay

Get screen list

qdbus6 org.plasmazones /PlasmaZones org.plasmazones.Screen.getScreens

Shell Scripting with gdbus

gdbus 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.

Calling methods

# 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'}"

Parsing JSON output with jq

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 .

Helper function

#!/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')"

Portable qdbus detection

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.getLayoutList

Shell Scripting with dbus-send

dbus-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:true

Shell Scripting with busctl

busctl 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

Monitoring Signals

gdbus monitor

# 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'

dbus-monitor

# 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'"

Key signals for scripting

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

Python Scripting

dasbus (recommended)

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()

pydbus

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()

dbus-python (legacy, widest availability)

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()

Recipes

Cycle to next layout

#!/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

Export all layouts to files

#!/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"

Auto-assign layout on monitor hotplug

#!/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

Log all events to a file

#!/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

Show screen and layout info

#!/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)")"'

Python: layout-per-activity automation

#!/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()

Scriptable Methods Reference

Layout Management

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

Screen Assignments

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

Per-Desktop Assignments

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

Overlay Control

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

Screen Info

Method Args Returns Description
getScreens() string[] All screen names
getScreenInfo(screen) string JSON Screen details
getScreenId(screen) string string Stable EDID-based ID

Settings

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

Window Tracking

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).


Tips

  • 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 jq in bash or json.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 symlink qdbus to it; check both in scripts.

Clone this wiki locally