SnailShell

Setting Wallpapers for Multiple Monitors through DBus for KDE Plasma


The Problem

KDE Plasma is my favorite Linux DE, and it has its quirks and warts. The support for multiple monitors, for example, is fairly buggy at times, especially when monitors are turned on and off. The way it deals with multiple monitors seems to be based on some sort of numerical index that doesn’t map itself to the monitors’ UUIDs, so when the configuration changes, desktop settings can be shifted around.

I have wallpapers of different shapes on each monitor, because my left monitor is vertical, the right horizontal, and they get messed up when the machine resumes from sleep or when the monitors are turned off manually. There appears to be no way of tying a wallpaper to a physical monitor. So, I have to frequently reset the appropriate wallpapers.

Plasma already provides a command-line tool for setting wallpaper: plasma-apply-wallpaperimage, but it doesn’t have the ability to address each monitor either.

The Solution

Fortunately, there’s a Band-Aid for every wart, if you are comfortable making your own Band-Aid. And in this case, my problem can be “fixed” by resetting the wallpapers with a script.

Plasma can be scripted with JavaScript through its DBus interface, and there’s a fairly comprehensive documentation (The KDE Community, 2023).

The first step is getting the desktops in a deterministic order. Desktops can be retrieved by a global function desktops() which “returns an array of all desktops that currently exist”(The KDE Community, 2023). Each Desktop object has a screen property, a numerical ID of the associated monitor, but since Plasma doesn’t provide any hardware information or stable ID of the monitor, the only way to sort the monitors deterministically seems to be using the screen’s position. We sort them from left to right, which is both deterministic, and fairly intuitive from the human user’s perspective, arguably more so than UUIDs. Furthermore, since we are setting wallpapers in the order of the monitors, we can also rule out Desktop objects with no associated screen. These ideas are stolen from the superpaper project (Hänninen, 2019).

function getDesktops() {
    return desktops()
        .filter(d => d.screen != -1)
        .sort((a, b) => screenGeometry(a.screen).left - screenGeometry(b.screen).left);
}

The second piece of puzzle is setting the wallpaper to a Desktop object:

function setWallpaper(desktop, path) {
    desktop.wallpaperPlugin = "org.kde.image"
    desktop.currentConfigGroup = Array("Wallpaper", "org.kde.image", "General")
    desktop.writeConfig("Image", path)
}

To assign a list of images to all the desktops one by one:

// Imagine we have a variable called imageList, an array containing the 
// paths of the image files.
getDesktops().forEach(
    (desktop, i) => setWallpaper(desktop, imageList[i % imageList.length])
);

To set one wallpaper for one specific desktop:

// Assume the variable `desktop_id` is the (0-based) index of the
// monitor, counting from left to right.
// And the variable `image_path` being the path to the image.
setWallpaper(getDesktops()[desktop_id], image_path);

DBus Interface

To run this script, we use the DBus Interface of Plasma, which provides a function called evaluateScript:

qdbus org.kde.plasmashell /PlasmaShell org.kde.PlasmaShell.evaluateScript "..."

…or invoke it with your favorite DBus tooling.

Parameterize

evaluateScript only accepts a self-contained script. You can hard code the path of the images, but there’s no way to supply the image paths as arguments to it, so some sort of string manipulation is required if you want to point Plasma to some arbitrary image.

For me, someone whose home directory is already filled with various Python glues and ad hoc scripts, nothing beats some composable f-strings, string replacements, and a nice familiar command-line interface. Any language good with string manipulation and a convenient DBus interface is a suitable tool for this step. Or, alternatively, you could also leave the path hard-coded, and place or link different images to that path before calling the script.

Here’s my script. I can set wallpapers to all the desktops by calling python wallpaper.py all image1.jpg image2.jpg ..., and set wallpaper for the 2nd monitor from the left with python wallpaper.py one 1 image1.jpg.

import dbus
import typer


SCRIPT_GET_DESKTOPS = """
function getDesktops() {
    return desktops()
        .filter(d => d.screen != -1)
        .sort((a, b) => screenGeometry(a.screen).left - screenGeometry(b.screen).left);
}
"""

SCRIPT_SET_WALLPAPER = """
function setWallpaper(desktop, path) {
    desktop.wallpaperPlugin = "org.kde.image"
    desktop.currentConfigGroup = Array("Wallpaper", "org.kde.image", "General")
    desktop.writeConfig("Image", path)
}
"""

SCRIPT_ALL = f"""
{SCRIPT_GET_DESKTOPS}
{SCRIPT_SET_WALLPAPER}
const imageList = IMAGE_LIST;
getDesktops().forEach((desktop, i) => setWallpaper(desktop, imageList[i % imageList.length]));
"""


SCRIPT_ONE = f"""
{SCRIPT_GET_DESKTOPS}
{SCRIPT_SET_WALLPAPER}
setWallpaper(getDesktops()[DESKTOP_ID], IMAGE);
"""


def quote(s):
    return "'" + s + "'"


def plasma_dbus():
    bus = dbus.SessionBus()
    plasma = dbus.Interface(
        bus.get_object("org.kde.plasmashell", "/PlasmaShell"), dbus_interface="org.kde.PlasmaShell"
    )
    return plasma


app = typer.Typer()


@app.command()
def all(image_path: list[str], generate: bool = False):
    image_list_string = "[" + ",".join(quote(p) for p in image_path) + "]"
    script = SCRIPT_ALL.replace("IMAGE_LIST", image_list_string)
    if generate:
        print(script)
    else:
        plasma_dbus().evaluateScript(script)


@app.command()
def one(desktop_id: int, image_path: str, generate: bool = False):
    script = SCRIPT_ONE.replace("IMAGE", quote(image_path)).replace("DESKTOP_ID", str(desktop_id))
    if generate:
        print(script)
    else:
        plasma_dbus().evaluateScript(script)


if __name__ == "__main__":
    app()

References

Hänninen, H. (2019). Superpaper [Python]. superpaper/wallpaper_processing.py at 219f00aec19a4f0697e37663875eccbfa19b502b · hhannine/superpaper · GitHub (Original work published 2019)

The KDE Community. (2023). API documentation for Plasma scripting API. Developer. API documentation | Developer